primitive 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,98 +0,0 @@
1
- import ow from 'ow'
2
-
3
- import { cssrgba } from './color'
4
-
5
- export const PARTIALS = true
6
- export const platform = 'browser'
7
-
8
- export const loadImage = async (input) => {
9
- ow(input, ow.any(
10
- ow.string.nonEmpty.label('input'),
11
- ow.object.instanceOf(global.ImageData).label('input'),
12
- ow.object.instanceOf(global.Image).label('input')
13
- ))
14
-
15
- if (typeof input === 'string') {
16
- const img = new global.Image()
17
- img.crossOrigin = 'anonymous'
18
-
19
- await new Promise((resolve, reject) => {
20
- img.onload = resolve
21
- img.onerror = img.onabort = () => reject(new Error('image failed to load'))
22
- img.src = input
23
- })
24
-
25
- return loadImage(img)
26
- } else if (input instanceof global.ImageData) {
27
- return input
28
- } else if (input instanceof global.Image) {
29
- input.crossOrigin = 'anonymous'
30
- const canvas = document.createElement('canvas')
31
- canvas.width = input.naturalWidth
32
- canvas.height = input.naturalHeight
33
- const ctx = canvas.getContext('2d')
34
- ctx.drawImage(input, 0, 0)
35
- return ctx.getImageData(0, 0, canvas.width, canvas.height)
36
- } else {
37
- throw new Error('invalid input image')
38
- }
39
- }
40
-
41
- export const loadCanvas = async (value, label = 'canvas') => {
42
- ow(value, ow.any(
43
- ow.string.nonEmpty.label(label),
44
- ow.object.instanceOf(global.HTMLCanvasElement).label(label)
45
- ))
46
-
47
- if (typeof value === 'string') {
48
- const canvas = document.querySelector(value)
49
-
50
- if (!canvas) {
51
- throw new Error(`invalid ${label} selector "${value}" not found`)
52
- }
53
-
54
- if (!(canvas instanceof global.HTMLCanvasElement)) {
55
- throw new Error(`invalid ${label} selector "${value}" is not a canvas element`)
56
- }
57
-
58
- return canvas
59
- } else {
60
- return value
61
- }
62
- }
63
-
64
- export const enableContextAntialiasing = (ctx) => {
65
- ctx.mozImageSmoothingQuality = 'high'
66
- ctx.webkitImageSmoothingQuality = 'high'
67
- ctx.msImageSmoothingQuality = 'high'
68
- ctx.imageSmoothingQuality = 'high'
69
-
70
- ctx.mozImageSmoothingEnabled = true
71
- ctx.webkitImageSmoothingEnabled = true
72
- ctx.msImageSmoothingEnabled = true
73
- ctx.imageSmoothingEnabled = true
74
- }
75
-
76
- export const createImage = (width, height, fillColor = undefined) => {
77
- ow(width, ow.number.label('width').positive.integer)
78
- ow(height, ow.number.label('height').positive.integer)
79
-
80
- const canvas = document.createElement('canvas')
81
- canvas.width = width
82
- canvas.height = height
83
- const ctx = canvas.getContext('2d')
84
- if (fillColor) {
85
- ctx.fillStyle = cssrgba(fillColor)
86
- ctx.fillRect(0, 0, width, height)
87
- }
88
- return ctx.getImageData(0, 0, width, height)
89
- }
90
-
91
- export default {
92
- PARTIALS,
93
- platform,
94
- loadImage,
95
- loadCanvas,
96
- enableContextAntialiasing,
97
- createImage
98
- }
package/lib/color.js DELETED
@@ -1,6 +0,0 @@
1
- import chromatism from 'chromatism'
2
- export default chromatism
3
-
4
- export const cssrgba = (color) => {
5
- return `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a / 255})`
6
- }
package/lib/context.js DELETED
@@ -1,101 +0,0 @@
1
- import execa from 'execa'
2
- import fs from 'fs'
3
- import ndarray from 'ndarray'
4
- import ow from 'ow'
5
- import pify from 'pify'
6
- import pump from 'pump-promise'
7
- import getPixels from 'get-pixels'
8
- import savePixels from 'save-pixels'
9
-
10
- const getPixelsP = pify(getPixels)
11
-
12
- export const PARTIALS = true
13
- export const platform = 'node'
14
-
15
- export const loadImage = async (input) => {
16
- ow(input, ow.string.label('input').nonEmpty)
17
-
18
- const result = await getPixelsP(input)
19
- const { data, shape } = result
20
-
21
- return {
22
- data,
23
- width: shape[0],
24
- height: shape[1]
25
- }
26
- }
27
-
28
- export const createImage = (width, height, color = undefined) => {
29
- ow(width, ow.number.label('width').positive.integer)
30
- ow(height, ow.number.label('height').positive.integer)
31
-
32
- const data = new Uint8ClampedArray(width * height * 4)
33
-
34
- if (color) {
35
- for (let i = 0; i < width * height; ++i) {
36
- const o = i * 4
37
- data[o + 0] = color.r
38
- data[o + 1] = color.g
39
- data[o + 2] = color.b
40
- data[o + 3] = color.a
41
- }
42
- }
43
-
44
- return {
45
- data,
46
- width,
47
- height
48
- }
49
- }
50
-
51
- export const saveImage = async (image, filename, opts) => {
52
- ow(image, ow.object.label('image').nonEmpty)
53
- ow(filename, ow.string.label('filename').nonEmpty)
54
-
55
- const pixels = ndarray(image.data, [ image.height, image.width, 4 ])
56
- const parts = filename.split('.')
57
- const format = parts[parts.length - 1]
58
- const stream = savePixels(pixels.transpose(1, 0, 2), format)
59
- return pump(stream, fs.createWriteStream(filename))
60
- }
61
-
62
- export const saveGIF = async (frames, filename, opts) => {
63
- ow(frames, ow.array.label('frames'))
64
- ow(filename, ow.string.label('filename').nonEmpty)
65
- ow(opts, ow.object.label('opts').plain.nonEmpty)
66
-
67
- const {
68
- // gif output options
69
- gifski = {
70
- fps: 10,
71
- quality: 80,
72
- fast: false
73
- }
74
- } = opts
75
-
76
- const params = [
77
- '-o', filename,
78
- '--fps', gifski.fps,
79
- gifski.fast && '--fast',
80
- '--quality', gifski.quality,
81
- '-W', 600, // TODO: make this configurable
82
- '--quiet'
83
- ]
84
- .concat(frames)
85
- .filter(Boolean)
86
-
87
- const executable = process.env.GIFSKI_PATH || 'gifski'
88
- const cmd = [ executable ].concat(params).join(' ')
89
- if (opts.log) opts.log(cmd)
90
-
91
- await execa.shell(cmd)
92
- }
93
-
94
- export default {
95
- PARTIALS,
96
- platform,
97
- loadImage,
98
- createImage,
99
- saveImage,
100
- saveGIF
101
- }
@@ -1,31 +0,0 @@
1
- import test from 'ava'
2
- import fs from 'fs'
3
- import path from 'path'
4
- import rmfr from 'rmfr'
5
- import tempy from 'tempy'
6
-
7
- import context from './context'
8
-
9
- const fixtures = path.join(__dirname, '..', 'media')
10
-
11
- test('monalisa.png', async (t) => {
12
- const img0 = await context.loadImage(path.join(fixtures, 'monalisa.png'))
13
- const temp = tempy.file({ extension: 'png' })
14
- await context.saveImage(img0, temp)
15
- const img1 = await context.loadImage(temp)
16
-
17
- t.deepEqual(img0.data, img1.data)
18
- await rmfr(temp)
19
- })
20
-
21
- test('flower.jpg', async (t) => {
22
- const img0 = await context.loadImage(path.join(fixtures, 'flower.jpg'))
23
- const temp = tempy.file({ extension: 'png' })
24
- await context.saveImage(img0, temp)
25
- const img1 = await context.loadImage(temp)
26
-
27
- t.true(fs.existsSync(temp))
28
- t.is(img0.width, img1.width)
29
- t.is(img0.height, img1.height)
30
- await rmfr(temp)
31
- })
package/lib/core.js DELETED
@@ -1,199 +0,0 @@
1
- 'use strict'
2
-
3
- export const getMeanColor = (image) => {
4
- const { data, width, height } = image
5
-
6
- let r = 0
7
- let g = 0
8
- let b = 0
9
-
10
- for (let i = 0; i < height; ++i) {
11
- for (let j = 0; j < width; ++j) {
12
- const o = (i * width + j) * 4
13
- r += data[o + 0]
14
- g += data[o + 1]
15
- b += data[o + 2]
16
- }
17
- }
18
-
19
- r = r / (width * height) | 0
20
- g = g / (width * height) | 0
21
- b = b / (width * height) | 0
22
-
23
- return { r, g, b, a: 255 }
24
- }
25
-
26
- export const computeColor = (target, current, lines, alpha) => {
27
- const { width } = target
28
- const dataT = target.data
29
- const dataC = current.data
30
-
31
- const a = 255.0 / alpha
32
-
33
- let count = 0
34
- let r = 0.0
35
- let g = 0.0
36
- let b = 0.0
37
-
38
- for (let i = 0; i < lines.length; ++i) {
39
- const line = lines[i]
40
-
41
- for (let j = line.x1; j <= line.x2; ++j) {
42
- const o = (line.y * width + j) * 4
43
-
44
- const tr = dataT[o + 0]
45
- const tg = dataT[o + 1]
46
- const tb = dataT[o + 2]
47
-
48
- const cr = dataC[o + 0]
49
- const cg = dataC[o + 1]
50
- const cb = dataC[o + 2]
51
-
52
- r += (tr - cr) * a + cr
53
- g += (tg - cg) * a + cg
54
- b += (tb - cb) * a + cb
55
-
56
- ++count
57
- }
58
- }
59
-
60
- if (count > 0) {
61
- r = Math.max(0, Math.min(255, (r / count))) | 0
62
- g = Math.max(0, Math.min(255, (g / count))) | 0
63
- b = Math.max(0, Math.min(255, (b / count))) | 0
64
- }
65
-
66
- return { r, g, b, a: alpha }
67
- }
68
-
69
- export const difference = (imageA, imageB) => {
70
- const { width, height } = imageA
71
- const dataA = imageA.data
72
- const dataB = imageB.data
73
-
74
- if (dataA.length !== dataB.length) {
75
- throw new Error('image.difference incompatible images')
76
- }
77
-
78
- let sum = 0.0
79
-
80
- for (let i = 0; i < height; ++i) {
81
- for (let j = 0; j < width; ++j) {
82
- const o = (i * width + j) * 4
83
-
84
- const ar = dataA[o + 0]
85
- const ag = dataA[o + 1]
86
- const ab = dataA[o + 2]
87
-
88
- const br = dataB[o + 0]
89
- const bg = dataB[o + 1]
90
- const bb = dataB[o + 2]
91
-
92
- const dr = ar - br
93
- const dg = ag - bg
94
- const db = ab - bb
95
-
96
- sum += (dr * dr + dg * dg + db * db)
97
- }
98
- }
99
-
100
- return Math.sqrt(sum / (width * height * 3)) / 255
101
- }
102
-
103
- export const differencePartial = (target, before, after, score, lines) => {
104
- const { width, height } = target
105
- const dataT = target.data
106
- const dataB = before.data
107
- const dataA = after.data
108
-
109
- if (dataT.length !== dataB.length || dataT.length !== dataA.length) {
110
- throw new Error('image.differencePartial incompatible images')
111
- }
112
-
113
- let sum = Math.pow(score * 255, 2) * width * height * 3
114
-
115
- for (let i = 0; i < lines.length; ++i) {
116
- const line = lines[i]
117
-
118
- for (let j = line.x1; j <= line.x2; ++j) {
119
- const o = (line.y * width + j) * 4
120
-
121
- const tr = dataT[o + 0]
122
- const tg = dataT[o + 1]
123
- const tb = dataT[o + 2]
124
-
125
- const br = dataB[o + 0]
126
- const bg = dataB[o + 1]
127
- const bb = dataB[o + 2]
128
-
129
- const ar = dataA[o + 0]
130
- const ag = dataA[o + 1]
131
- const ab = dataA[o + 2]
132
-
133
- const dr1 = tr - br
134
- const dg1 = tg - bg
135
- const db1 = tb - bb
136
-
137
- const dr2 = tr - ar
138
- const dg2 = tg - ag
139
- const db2 = tb - ab
140
-
141
- sum -= (dr1 * dr1 + dg1 * dg1 + db1 * db1)
142
- sum += (dr2 * dr2 + dg2 * dg2 + db2 * db2)
143
- }
144
- }
145
-
146
- return Math.sqrt(sum / (width * height * 3)) / 255
147
- }
148
-
149
- export const copyLines = (dest, src, lines) => {
150
- const { width, height } = src
151
- const m = width * height * 4
152
-
153
- if (dest.data.length !== src.data.length) {
154
- throw new Error('image.copyLines incompatible images')
155
- }
156
-
157
- for (let i = 0; i < lines.length; ++i) {
158
- const line = lines[i]
159
- const o1 = Math.min(m, (line.y * width + line.x1) * 4)
160
- const o2 = Math.min(m, (line.y * width + line.x2) * 4)
161
- dest.data.set(src.data.slice(o1, o2), o1)
162
- }
163
- }
164
-
165
- export const drawLines = (image, color, lines) => {
166
- const { data, width } = image
167
-
168
- const sr = color.r
169
- const sg = color.g
170
- const sb = color.b
171
- const sa = color.a
172
-
173
- for (let i = 0; i < lines.length; ++i) {
174
- const line = lines[i]
175
- const ma = sa / 255
176
- const a = 1.0 - ma
177
-
178
- for (let j = line.x1; j <= line.x2; ++j) {
179
- const o = (line.y * width + j) * 4
180
-
181
- const dr = data[o + 0]
182
- const dg = data[o + 1]
183
- const db = data[o + 2]
184
-
185
- data[o + 0] = (dr * a + sr * ma) | 0
186
- data[o + 1] = (dg * a + sg * ma) | 0
187
- data[o + 2] = (db * a + sb * ma) | 0
188
- }
189
- }
190
- }
191
-
192
- export default {
193
- getMeanColor,
194
- computeColor,
195
- difference,
196
- differencePartial,
197
- copyLines,
198
- drawLines
199
- }
package/lib/core.test.js DELETED
@@ -1,70 +0,0 @@
1
- import test from 'ava'
2
- import path from 'path'
3
-
4
- import context from './context'
5
- import core from './core'
6
- import Scanline from './scanline'
7
-
8
- const fixtures = path.join(__dirname, '..', 'media')
9
-
10
- test('difference', async (t) => {
11
- const image = await context.loadImage(path.join(fixtures, 'monalisa.png'))
12
- const color = core.getMeanColor(image)
13
- const blank = context.createImage(image.width, image.height)
14
- const current = context.createImage(image.width, image.height, color)
15
-
16
- const diff0 = core.difference(image, blank)
17
- t.true(diff0 > 0)
18
-
19
- const diff1 = core.difference(image, current)
20
- t.true(diff1 > 0)
21
- t.true(diff0 > diff1)
22
-
23
- const diff2 = core.difference(image, image)
24
- t.is(diff2, 0)
25
-
26
- let diff = diff0
27
- for (let i = 0; i < image.height; ++i) {
28
- const o = i * image.width * 4
29
- blank.data.set(image.data.slice(o, o + image.width * 4), o)
30
- const diff3 = core.difference(image, blank)
31
- t.true(diff3 < diff)
32
- diff = diff3
33
- }
34
- t.is(diff, 0)
35
- })
36
-
37
- test('drawLines', async (t) => {
38
- const image = await context.loadImage(path.join(fixtures, 'monalisa.png'))
39
- const color = core.getMeanColor(image)
40
- const current = context.createImage(image.width, image.height, color)
41
-
42
- const diff0 = core.difference(image, current)
43
- t.true(diff0 > 0)
44
-
45
- const lines = []
46
- const c = { r: 255, g: 0, b: 0, a: 255 }
47
- const m = image.width / 2 | 0
48
-
49
- for (let i = 0; i < 16; ++i) {
50
- lines.push(new Scanline(i, 5, m, 255))
51
- }
52
-
53
- t.is(lines.length, Scanline.filter(lines, image.width, image.height).length)
54
-
55
- core.drawLines(current, c, lines)
56
-
57
- for (let i = 0; i < 16; ++i) {
58
- for (let j = 5; j < m; ++j) {
59
- const o = (i * image.width + j) * 4
60
- t.is(current.data[o + 0], 255)
61
- t.is(current.data[o + 1], 0)
62
- t.is(current.data[o + 2], 0)
63
- t.is(current.data[o + 3], 255)
64
- }
65
- }
66
-
67
- const diff1 = core.difference(image, current)
68
- t.true(diff1 > 0)
69
- t.true(diff1 > diff0)
70
- })
package/lib/model.js DELETED
@@ -1,168 +0,0 @@
1
- import * as chromatism from 'chromatism'
2
- import ow from 'ow'
3
-
4
- import core from './core'
5
- import optimize from './optimize'
6
- import Worker from './worker'
7
-
8
- export default class Model {
9
- constructor (opts) {
10
- const {
11
- context,
12
- target,
13
- backgroundColor,
14
- outputSize,
15
- numCandidates = 1
16
- } = opts
17
-
18
- const { width, height } = target
19
- const aspect = width / height
20
-
21
- if (outputSize) {
22
- if (aspect >= 1) {
23
- this.sw = outputSize
24
- this.sh = outputSize / aspect | 0
25
- this.scale = outputSize / width
26
- } else {
27
- this.sw = outputSize * aspect | 0
28
- this.sh = outputSize
29
- this.scale = outputSize / height
30
- }
31
- } else {
32
- this.sw = width
33
- this.sh = height
34
- this.scale = 1
35
- }
36
-
37
- this.context = context
38
- this.target = target
39
- this.backgroundColor = backgroundColor
40
-
41
- this.current = this.createImage()
42
- this.score = core.difference(this.target, this.current)
43
-
44
- this.shapes = []
45
- this.colors = []
46
- this.scores = []
47
- this.workers = []
48
-
49
- for (let i = 0; i < numCandidates; ++i) {
50
- this.workers.push(new Worker({
51
- context,
52
- target
53
- }))
54
- }
55
-
56
- if (this.context.PARTIALS) {
57
- this.before = this.context.createImage(width, height)
58
- }
59
- }
60
-
61
- createImage () {
62
- const { width, height } = this.target
63
- return this.context.createImage(width, height, this.backgroundColor)
64
- }
65
-
66
- add (shape, alpha) {
67
- const lines = shape.rasterize()
68
- const color = core.computeColor(this.target, this.current, lines, alpha)
69
- let score
70
-
71
- if (this.context.PARTIALS) {
72
- this.before.data.set(this.current.data)
73
- core.drawLines(this.current, color, lines)
74
- score = core.differencePartial(this.target, this.before, this.current, this.score, lines)
75
- } else {
76
- core.drawLines(this.current, color, lines)
77
- score = core.difference(this.target, this.current)
78
- }
79
-
80
- this.score = score
81
- this.shapes.push(shape)
82
- this.colors.push(color)
83
- this.scores.push(score)
84
- }
85
-
86
- step (opts) {
87
- ow(opts, ow.object.plain)
88
- ow(opts.shapeType, ow.string.nonEmpty)
89
- ow(opts.shapeAlpha, ow.number.integer.positive)
90
- ow(opts.numCandidateShapes, ow.number.integer.positive)
91
- ow(opts.numCandidateMutations, ow.number.integer.positive)
92
-
93
- let state = this._getBestCandidateState(opts)
94
- this.add(state.shape, state.alpha)
95
-
96
- if (opts.numCandidateExtras) {
97
- ow(opts.numCandidateExtras, ow.number.integer)
98
-
99
- for (let i = 0; i < opts.numCandidateExtras; ++i) {
100
- state.worker.init(this.current, this.score)
101
- const a = state.energy()
102
- state = optimize.hillClimb(state, opts.numCandidateMutations)
103
- const b = state.energy()
104
- if (b <= a) break
105
-
106
- this.add(state.shape, state.alpha)
107
- }
108
- }
109
-
110
- return this.workers
111
- .reduce((sum, worker) => sum + worker.counter, 0)
112
- }
113
-
114
- _getBestCandidateState (opts) {
115
- const states = this.workers
116
- .map((worker) => {
117
- worker.init(this.current, this.score)
118
- return optimize.getBestHillClimbState(worker, opts)
119
- })
120
-
121
- let bestEnergy = null
122
- let bestState = null
123
-
124
- for (let i = 0; i < states.length; ++i) {
125
- const state = states[i]
126
- const energy = state.energy()
127
-
128
- if (!i || energy < bestEnergy) {
129
- bestEnergy = energy
130
- bestState = state
131
- }
132
- }
133
-
134
- return bestState
135
- }
136
-
137
- toSVG () {
138
- const bg = chromatism.convert(this.backgroundColor).hex
139
- const body = this.shapes
140
- .map((shape, index) => {
141
- const color = this.colors[index]
142
- const fill = chromatism.convert(color).hex
143
- const attrs = `fill="${fill}" fill-opacity="${color.a / 255}"`
144
- return shape.toSVG(attrs)
145
- })
146
- .join('\n')
147
-
148
- return `
149
- <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${this.sw}" height="${this.sh}">
150
- <rect x="0" y="0" width="${this.sw}" height="${this.sh}" fill="${bg}" />
151
- <g transform="scale(${this.scale}) translate(0.5 0.5)">
152
- ${body}
153
- </g>
154
- </svg>
155
- `
156
- }
157
-
158
- toFrames (scoreDelta = 0) {
159
- throw new Error('TODO')
160
-
161
- /*
162
- for (let i = 0; i < this.shapes.length; ++i) {
163
- const shape = this.shapes[i]
164
- const color = this.colors[i]
165
- }
166
- */
167
- }
168
- }