diff --git a/lib/stylus/README.txt b/lib/stylus/README.txt
new file mode 100644
index 0000000..e4da17e
--- /dev/null
+++ b/lib/stylus/README.txt
@@ -0,0 +1,13 @@
+
+
+https://mattdesl.svbtle.com/drawing-lines-is-hard
+http://perfectionkills.com/exploring-canvas-drawing-techniques/
+
+
+
+https://github.com/mattdesl/polyline-normals
+
+var path = [ [0, 122], [0, 190], [90, 190] ]
+
+//get the normals as a closed loop
+var normals = getNormals(path, true)
diff --git a/lib/stylus/index.html b/lib/stylus/index.html
new file mode 100644
index 0000000..51584c0
--- /dev/null
+++ b/lib/stylus/index.html
@@ -0,0 +1,73 @@
+
+
+
+  
+
+  
+  
+  Stylus Functional Tests
+  
+
+    
+	
+
+
+
+    
+
+
+
+
diff --git a/lib/stylus/main.js b/lib/stylus/main.js
new file mode 100644
index 0000000..d3ff5ce
--- /dev/null
+++ b/lib/stylus/main.js
@@ -0,0 +1,90 @@
+import PIXIApp from '../../lib/pixi/app.js'
+import Button from '../../lib/pixi/button.js'
+import ButtonGroup from '../../lib/pixi/buttongroup.js'
+import Stylus from './stylus.js'
+
+class StylusApp extends PIXIApp {
+
+    sceneFactory() {
+        return new Stylus(this.renderer)
+    }
+
+    setup() {
+        let buttonColor = 0x666666
+        super.setup()
+
+        this.tools = new ButtonGroup({
+            type: 'checkbox',
+            margin: 0,
+            x: 16,
+            y: 16,
+            fill: buttonColor,
+            buttons: [{icon: 'edit',
+                        iconColorActive: 0xFFFF00,
+                        action: (event, button) => this.toggleEditMode() },
+               {icon: 'undo',
+                        action: (event, button) => this.undo(button) },
+               {icon: 'redo',
+                        action: (event, button) => this.redo(button) },
+               {icon: 'delete',
+                        action: (event, button) => this.clear(button) }
+            ]
+        })
+        this.scene.addChild(this.tools)
+
+        let defaults = { icon: 'brightness_1',
+                    action: (event, button) => this.selectColor(button),
+                    fillAlpha: 0,
+                    strokeAlpha: 0,
+                    fillActiveAlpha: 0,
+                    strokeActiveAlpha: 0}
+
+        this.palette = new ButtonGroup( {
+            type: "radio",
+            x: 200,
+            y: 16,
+            margin: 0,
+            strokeAlpha: 0,
+            fill: buttonColor,
+            buttons: [
+                Object.assign({}, defaults, { iconColor: 0x111111,
+                                                iconColorActive: 0x111111}),     // tooltip: "Black",
+                Object.assign({}, defaults, { iconColor: 0xFFFF00,
+                                                iconColorActive: 0xFFFF00}),     // tooltip: "Yellow",
+                Object.assign({}, defaults, { iconColor: 0x00FF00,
+                                                iconColorActive:0x00FF00}),          // tooltip: "Green",
+                Object.assign({}, defaults, { iconColor: 0xFF00FF,
+                                                iconColorActive:0xFF00FF})     // tooltip: "Violet",
+            ]
+        })
+        this.scene.addChild(this.palette)
+    }
+
+    selectColor(button) {
+        this.scene.color = button.opts.iconColor
+    }
+
+    undo(button) {
+        this.scene.undo()
+        setTimeout(() => {
+            button.active = false}, 200)
+    }
+
+    redo(button) {
+        this.scene.redo()
+        setTimeout(() => {
+            button.active = false}, 200)
+    }
+
+    clear(button) {
+        this.scene.clearAll()
+        setTimeout(() => {
+            button.active = false}, 200)
+    }
+}
+
+const app = new StylusApp({ view: canvas })
+window.app = app
+app.setup()
+app.run()
+
diff --git a/lib/stylus/stylus.js b/lib/stylus/stylus.js
new file mode 100644
index 0000000..a267416
--- /dev/null
+++ b/lib/stylus/stylus.js
@@ -0,0 +1,398 @@
+import Events from '../../node_modules/iwmlib/lib/events.js'
+import { Angle } from '../../node_modules/iwmlib/lib/utils.js'
+
+class StylusCommand extends Object {
+
+    constructor() {
+        super()
+    }
+
+    do(stylus) {
+        stylus.commandStack.push(this)
+    }
+
+    undo(stylus) {
+        stylus.undoCommandStack.push(this)
+    }
+
+    redo(stylus) {
+        this.do(stylus)
+    }
+}
+
+class StrokeCommand extends StylusCommand {
+
+    constructor(stroke) {
+        super()
+        this.stroke = stroke
+    }
+
+    do(stylus) {
+        if (this.stroke.length > 0) {
+            super.do(stylus)
+            stylus.stroke = []
+            stylus.strokes.push(this.stroke)
+            stylus.redraw()
+            stylus.changed()
+        }
+    }
+
+    undo(stylus) {
+        if (this.stroke.length > 0) {
+            super.undo(stylus)
+            stylus.strokes.pop()
+            stylus.redraw()
+            stylus.changed()
+        }
+    }
+}
+
+class ClearCommand extends StylusCommand {
+
+    do(stylus) {
+        // Clears the command stack
+        stylus.commandStack = []
+        super.do(stylus)
+        this.strokes = stylus.strokes
+        stylus.stroke = []
+        stylus.strokes = []
+        stylus.redraw()
+        stylus.changed()
+    }
+
+    undo(stylus) {
+        //super.undo(stylus) // Clear all is not redoable
+        stylus.stroke = []
+        stylus.strokes = this.strokes
+        stylus.redraw()
+        stylus.changed()
+    }
+}
+
+
+export default class Stylus extends PIXI.Graphics {
+
+    constructor({ width = window.innerWidth,
+        height = window.innerHeight,
+        interactive = true,
+        color = 0x000000,
+        tiltX = 0,
+        tiltY = 0,
+        backgroundAlpha = 1,
+        backgroundFill = 0xFFFFFF,
+        colorAlpha = 1,
+        captureEvents = true,
+        acceptMouseEvents = true } = {}) {
+        super()
+        this.wantedWidth = width
+        this.wantedHeight = height
+        this.backgroundAlpha = backgroundAlpha
+        this.backgroundFill = backgroundFill
+        this.colorAlpha = colorAlpha
+        this.color = color
+        this.interactive = interactive
+        this.debug = false
+        this.tiltX = tiltX  // degrees -90 ... 90
+        this.tiltY = tiltY  // degrees -90 ... 90
+        this.captureEvents = captureEvents
+        this.commandStack = []
+        this.undoCommandStack = []
+        this.strokes = []
+        this.stroke = []
+        if (captureEvents)
+            this.registerEventHandler(acceptMouseEvents)
+        this.drawBackground()
+    }
+
+    drawBackground() {
+        this.clear()
+        this.beginFill(this.backgroundFill, this.backgroundAlpha)
+        this.drawRect(0, 0, this.wantedWidth, this.wantedHeight)
+        this.endFill()
+    }
+
+    touchToPoint(t) {
+        return { x: t.clientX, y: t.clientY }
+    }
+
+    isStylusPointer(event) {
+        let identifier = event.data.identifier
+        if (typeof (event.data.originalEvent.changedTouches) !== 'undefined') {
+            for (let touch of event.data.originalEvent.changedTouches) {
+                if (touch.identifier === identifier && touch.touchType === 'stylus') {
+                    this.tiltX = Angle.radian2degree(touch.azimuthAngle)
+                    this.tiltY = 90.0 - Angle.radian2degree(touch.altitudeAngle)
+                    return true
+                }
+            }
+        }
+        // UO: Not tested since the Sprot delivered "mouse" events to Chrome
+        if (event.data.originalEvent.pointerType === 'pen') {
+            this.tiltX = event.data.originalEvent.tiltX
+            this.tiltY = event.data.originalEvent.tiltY
+            return true
+        }
+        return false
+    }
+
+    isStylusTouch(event) {
+        let identifier = event.data.identifier
+        if (typeof (event.data.originalEvent.changedTouches) !== 'undefined') {
+            for (let touch of event.data.originalEvent.changedTouches) {
+                if (touch.identifier === identifier && touch.pointerType === 'touch') {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
+    getPointerID(event) {
+        let identifier = event.data.identifier
+        for (let touch of event.data.originalEvent.changedTouches) {
+            if (touch.identifier === identifier) {
+                return touch.pointerId
+            }
+        }
+    }
+
+    registerEventHandler() {
+        window.addEventListener('keydown', (e) => {
+            switch (e.keyCode) {
+                case 38:  // up arrow
+                    this.tiltX += 5
+                    break
+                case 40:  // down arrow
+                    this.tiltX -= 5
+                    break
+                case 37:  // left arrow
+                    this.tiltY -= 5
+                    break
+                case 39:  // right arrow
+                    this.tiltY += 5
+                    break
+            }
+            if (this.debug) console.log("keydown", e.keyCode, this.tiltX, this.tiltY)
+        })
+
+        this.on('pointerdown', (e) => {
+            if (this.debug) console.log("pointerdown", e)
+            if (this.eventInside(e))
+                this.startStroke(this.toStroke(e))
+        })
+        this.on('pointermove', (e) => {
+            if (Events.isPointerDown(e.data.originalEvent) || this.isStylusPointer(e) || this.isStylusTouch(e)) {
+                if (this.debug) console.log("pointermove", e, this.eventInside(e))
+                if (this.eventInside(e))
+                    this.moveStroke(this.toStroke(e))
+            }
+        })
+        this.on('pointerup', (e) => {
+            if (this.debug) console.log("pointerup", e)
+            if (this.eventInside(e))
+                this.endStroke(this.toStroke(e))
+        })
+        this.on('pointerleave', (e) => {
+            this.endStroke(this.toStroke(e))
+        })
+        this.on('pointercancel', (e) => {
+            this.endStroke(this.toStroke(e))
+        })
+    }
+
+    undoable() {
+        return this.commandStack.length > 0
+    }
+
+    redoable() {
+        return this.undoCommandStack.length > 0
+    }
+
+    undo() {
+        if (this.undoable()) {
+            let cmd = this.commandStack.pop()
+            cmd.undo(this)
+        }
+    }
+
+    redo() {
+        if (this.redoable()) {
+            let cmd = this.undoCommandStack.pop()
+            cmd.redo(this)
+        }
+    }
+
+    eventInside(event) {
+
+        let local = this.toLocal(event.data.global)
+        for (let child of this.children) {
+            let r = child.getBounds()
+            if (r.contains(local.x, local.y)) {
+                console.log("Child touched")
+                return false
+            }
+        }
+        if (local.x < 0 || local.x > this.wantedWidth)
+            return false
+        if (local.y < 0 || local.y > this.wantedHeight)
+            return false
+        event.stopPropagation()
+        if (this.debug) console.log("stopPropagation", event)
+        if (event.data.originalEvent.claimedByScatter) {
+            return false
+        }
+        return true
+    }
+
+    toLocalPoint(event) {
+        return this.toLocal(event.data.global)
+    }
+
+    toStroke(event) {
+        let local = this.toLocalPoint(event)
+        let x = Math.max(0, Math.min(local.x, this.wantedWidth))
+        let y = Math.max(0, Math.min(local.y, this.wantedHeight))
+        let desc = {
+            x, y,
+            pressure: event.pressure || null,
+            tiltX: this.tiltX, tiltY: this.tiltY,
+            color: this.color
+        }
+        return desc
+    }
+
+    startStroke(info) {
+        this.stroke = [info]
+        this.redraw()
+    }
+
+    moveStroke(info) {
+        this.stroke.push(info)
+        this.redraw()
+    }
+
+    endStroke(info) {
+        if (this.stroke.length > 1) {
+            let cmd = new StrokeCommand(this.stroke)
+            cmd.do(this)
+        }
+    }
+
+    tiltToLineWidth(value) {
+        return Math.round(Math.abs(value / 10) + 1)
+    }
+
+    drawStroke(stroke) {
+        if (stroke.length) {
+            let start = stroke[0]
+            this.beginFill(0, 0)
+            this.moveTo(start.x, start.y)
+            for (let i = 1; i < stroke.length; i++) {
+                let info = stroke[i]
+                this.lineStyle(this.tiltToLineWidth(info.tiltY),
+                    info.color, this.colorAlpha)
+                this.lineTo(info.x, info.y)
+            }
+            this.endFill()
+        }
+    }
+
+    drawTouch(point) {
+        this.beginFill(0, 0)
+        this.drawCircle(point.x, point.y, 22)
+        this.endFill()
+    }
+
+    drawStrokes() {
+        this.drawBackground()
+        this.lineStyle(1.0, 0xFF0000, 1)
+        for (let stroke of this.iterStrokes()) {
+            this.drawStroke(stroke)
+        }
+    }
+
+    redraw() {
+        this.drawStrokes()
+    }
+
+    // Can be overwritten if different levels of strokes are necessary
+    *iterStrokes() {
+        for (let stroke of this.strokes) {
+            yield stroke
+        }
+        yield this.stroke
+    }
+
+    changed() {
+        // Can be overwritten
+    }
+
+    clearAll() {
+        let cmd = new ClearCommand()
+        cmd.do(this)
+    }
+
+    normalizeInfo(info) {
+        let { x, y, pressure, tiltX, tiltY, color } = info
+        x /= this.wantedWidth
+        y /= this.wantedHeight
+        return { x, y, pressure, tiltX, tiltY, color }
+    }
+
+    denormalizeInfo(info) {
+        let { x, y, pressure, tiltX, tiltY, color } = info
+        x = x * this.wantedWidth
+        y = y * this.wantedHeight
+        return { x, y, pressure, tiltX, tiltY, color }
+    }
+
+    // Convert strokes into an object that can be stored in an Indexed DB.
+    // Returns normalized strokes
+    toObject() {
+        let result = []
+        for (let stroke of this.strokes) {
+            let normalized = []
+            for (let info of stroke) {
+                normalized.push(this.normalizeInfo(info))
+            }
+            result.push(normalized)
+        }
+        return result
+    }
+
+    // Read normalized strokes from an object from an Indexed DB.
+    fromObject(normalizedStrokes) {
+        this.strokes = []
+        for (let stroke of normalizedStrokes) {
+            let denormalized = []
+            for (let info of stroke) {
+                denormalized.push(this.denormalizeInfo(info))
+            }
+            this.strokes.push(denormalized)
+        }
+    }
+
+    // Convert strokes into a JSON object that can be stored in an Indexed DB
+    toJSON() {
+        return JSON.stringify(this.toObject())
+    }
+
+    // Convert strokes from a JSON
+    fromJSON(json) {
+        this.fromObject(JSON.parse(json))
+    }
+
+    // Returns a set of used colors
+    usedColors() {
+        let used = new Set()
+        for (let info of this.stroke) {
+            used.add(info.color)
+        }
+        for (let stroke of this.strokes) {
+            for (let info of stroke) {
+                used.add(info.color)
+            }
+        }
+        return used.values()
+    }
+}
diff --git a/lib/stylus/thumbnail.png b/lib/stylus/thumbnail.png
new file mode 100644
index 0000000..e2c7b2b
Binary files /dev/null and b/lib/stylus/thumbnail.png differ