Giter Club home page Giter Club logo

atrament's Introduction

Atrament

A small JS library for beautiful drawing and handwriting on the HTML Canvas


Atrament is a library for drawing and handwriting on the HTML canvas. Its goal is for drawing to feel natural and comfortable, and the result to be smooth and pleasing. Atrament does not store the stroke paths itself - instead, it draws directly onto the canvas bitmap, just like an ink pen onto a piece of paper ("atrament" means ink in Slovak and Polish). This makes it suitable for certain applications, and not quite ideal for others.

⚠️ Note: From version 4, Atrament supports evergeen browsers (Firefox, Chrome and Chromium-based browsers) and Safari 15 or above. If your application must support older browsers, please use version 3. You can view the v3 documentation here.

Features:

  • Draw/Fill/Erase modes
  • Adjustable adaptive smoothing
  • Events tracking the drawing - this allows the app to "replay" or reconstruct the drawing, e.g. for undo functionality
  • Adjustable line thickness and colour

Here's a basic demo.

Enjoy!

Installation

If you're using a tool like rollup or webpack to bundle your code, you can install it using npm.

  • install atrament as a dependency using npm install --save atrament.
  • You can access the Atrament class using import { Atrament } from 'atrament';

Usage

  • create a <canvas> tag, e.g.:
<canvas id="sketchpad" width="500" height="500"></canvas>
  • in your JavaScript, create an Atrament instance passing it your canvas object:
import Atrament from 'atrament';

const canvas = document.querySelector('#sketchpad');
const sketchpad = new Atrament(canvas);
const sketchpad = new Atrament(canvas, {
  width: 500,
  height: 500,
  color: 'orange',
});
  • that's it, happy drawing!

Options & config

  • clear the canvas:
sketchpad.clear();
  • change the line thickness:
sketchpad.weight = 20; //in pixels
  • change the color:
sketchpad.color = '#ff485e'; //just like CSS
  • toggle between modes:
import { MODE_DRAW, MODE_ERASE, MODE_FILL, MODE_DISABLED } from 'atrament';

sketchpad.mode = MODE_DRAW; // default
sketchpad.mode = MODE_ERASE; // eraser tool
sketchpad.mode = MODE_FILL; // click to fill area
sketchpad.mode = MODE_DISABLED; // no modification to the canvas (will still fire stroke events)
  • tweak smoothing - higher values make the drawings look smoother, lower values make drawing feel a bit more responsive. Set to 0.85 by default.
sketchpad.smoothing = 1.3;
  • toggle adaptive stroke, i.e. line width changing based on drawing speed and stroke progress. This simulates the variation in ink discharge of a physical pen. true by default.
sketchpad.adaptiveStroke = false;
  • record stroke data (enables the strokerecorded event). false by default.
sketchpad.recordStrokes = true;

Data model

  • Atrament models its output as a set of independent strokes. Only one stroke can be drawn at a time.
  • Each stroke consists of a list of segments, which correspond to all the pointer positions recorded during drawing.
  • Each segment consists of a point which contains x and y coordinates, and a time which is the number of milliseconds since the stroke began, until the segment was drawn.
  • Each stroke also contains information about the drawing settings at the time of drawing (see Events > Stroke recording).

High DPI screens

To make drawings look sharp on high DPI screens, Atrament scales its drawing context by window.devicePixelRatio since v4.0.0. This means when you set a custom width or height, you should also multiply the CSS pixel values by devicePixelRatio. The values accepted by draw() and included in stroke events are always in CSS pixels.

Events

Dirty/clean

These events fire when the canvas is first drawn on, and when it's cleared. The state is stored in the dirty property.

sketchpad.addEventListener('dirty', () => console.info(sketchpad.dirty));
sketchpad.addEventListener('clean', () => console.info(sketchpad.dirty));

Stroke start/end

These events don't provide any data - they just inform that a stroke has started/finished.

sketchpad.addEventListener('strokestart', () => console.info('strokestart'));
sketchpad.addEventListener('strokeend', () => console.info('strokeend'));

Fill start/end

These only fire in fill mode. The fillstart event also contains x and y properties denoting the starting point of the fill operation (where the user has clicked).

sketchpad.addEventListener('fillstart', ({ x, y }) =>
  console.info(`fillstart ${x} ${y}`),
);
sketchpad.addEventListener('fillend', () => console.info('fillend'));

Stroke recording

The following events only fire if the recordStrokes property is set to true.

strokerecorded fires at the same time as strokeend and contains data necessary for reconstructing the stroke. segmentdrawn fires during stroke recording every time the draw method is called. It contains the same data as strokerecorded.

sketchpad.addEventListener('strokerecorded', ({ stroke }) =>
  console.info(stroke),
);
/*
{
  segments: [
    {
      point: { x, y },
      time,
    }
  ],
  color,
  weight,
  smoothing,
  adaptiveStroke,
}
*/
sketchpad.addEventListener('segmentdrawn', ({ stroke }) =>
  console.info(stroke),
);

Programmatic drawing

To enable functionality such as undo/redo, stroke post-processing, and SVG export in apps using Atrament, the library can be configured to record and programmatically draw the strokes.

The first step is to enable recordStrokes, and add a listener for the strokerecorded event:

atrament.recordStrokes = true;
atrament.addEventListener('strokerecorded', ({ stroke }) => {
  // store `stroke` somewhere
});

The stroke can then be reconstructed using methods of the Atrament class:

// set drawing options
atrament.mode = stroke.mode;
atrament.weight = stroke.weight;
atrament.smoothing = stroke.smoothing;
atrament.color = stroke.color;
atrament.adaptiveStroke = stroke.adaptiveStroke;

// don't want to modify original data
const segments = stroke.segments.slice();

const firstPoint = segments.shift().point;
// beginStroke moves the "pen" to the given position and starts the path
atrament.beginStroke(firstPoint.x, firstPoint.y);

let prevPoint = firstPoint;
while (segments.length > 0) {
  const point = segments.shift().point;

  // the `draw` method accepts the current real coordinates
  // (i. e. actual cursor position), and the previous processed (filtered)
  // position. It returns an object with the current processed position.
  const { x, y } = atrament.draw(point.x, point.y, prevPoint.x, prevPoint.y);

  // the processed position is the one where the line is actually drawn to
  // so we have to store it and pass it to `draw` in the next step
  prevPoint = { x, y };
}

// endStroke closes the path
atrament.endStroke(prevPoint.x, prevPoint.y);

Implementing Undo/Redo

Atrament does not provide its own undo/redo functionality to keep the scope as small as possible. However, using stroke recording and programmatic drawing, it is possible to implement undo/redo with a relatively small amount of code. See @nidoro and @feored's example here.

Development

To obtain the dependencies, cd into the atrament directory and run npm install. You should be able to then build atrament by simply running npm run build.

I didn't bother writing tests because it's such a small package. Contributions are welcome!

atrament's People

Contributors

bencergazda avatar dependabot[bot] avatar dethe avatar future-cyborg avatar jakubfiala avatar panglesd avatar ptkach avatar reccanti avatar rubenanton90 avatar rubenstolk avatar uriberto avatar xxq0411 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

atrament's Issues

Undo implementation

Hello,
I'm getting unexpected results from my undo implementation. This is how I'm doing it:

1. Initialize (global) empty array of strokes:
let strokes = []

2. When strokerecorded is fired, push stroke to array:

atrament.addEventListener('strokerecorded', function(obj) {
    if (!atrament.recordPaused) {
        strokes.push(obj.stroke);
    }
});

3. When the undo button is pressed, do the following:

function undoStroke() {
    strokes.pop();
    atrament.clear();
    atrament.recordPaused = true;
    for (let stroke of strokes) {
        // set drawing options
        atrament.mode = stroke.mode;
        atrament.weight = stroke.weight;
        atrament.smoothing = stroke.smoothing;
        atrament.color = stroke.color;
        atrament.adaptiveStroke = stroke.adaptiveStroke;

        // don't want to modify original data
        const points = stroke.points.slice();

        const firstPoint = points.shift();
        // beginStroke moves the "pen" to the given position and starts the path
        atrament.beginStroke(firstPoint.x, firstPoint.y);

        let prevPoint = firstPoint;
        while (points.length > 0) {
            const point = points.shift();

            // the `draw` method accepts the current real coordinates
            // (i. e. actual cursor position), and the previous processed (filtered)
            // position. It returns an object with the current processed position.
            const { x, y } = atrament.draw(point.x, point.y, prevPoint.x, prevPoint.y);

            // the processed position is the one where the line is actually drawn to
            // so we have to store it and pass it to `draw` in the next step
            prevPoint = { x, y };
        }

        // endStroke closes the path
        atrament.endStroke(prevPoint.x, prevPoint.y);
    }
    atrament.recordPaused = false;
}

But this is what I get:
Peek 2021-05-25 16-15

Notice how after I press undo, the last line disappears, as expected, but the other two lines are extended a little bit.

Can you help me with this?
I'm on Linux. This happens on both Firefox 72.0.1 and Google Chrome 89.0.4389.90.

Thank you for the library!

Use a WebWorker for fill mode

Need to test this, but the following approach might really enhance UX of the fill mode.

  1. initiate filling, show loading cursor, block drawing until step 5
  2. send the ImageData to a WW
  3. let the WW fill the appropriate pixels while the user revels in their unblocked UI
  4. WW sends back ImageData, we use drawImage to render it as it's a lot faster.
  5. stop filling, unblock drawing, have fun

I'd say this should be pretty high priority as it's a major UX improvement especially for using atrament canvas in a larger app with lots of other UI.

Different behavior when Inspect Element is open on Chrome

Hi, I can't help but notice a difference when using Atrament with and without Inspect Element open on Chrome. It isn't a big issue, but is there a reason for this?

In this picture, the above is written with inspect open, and bottom is without. The above is more rough, and has 0 smoothing lag when writing, where as the version below (the one people will use) behaves properly.
image

I tried on this website: https://www.fiala.space/atrament.js/demo/

Accept options object in constructor

I guess it would be far more useful to have a signature like this

atrament(canvas, options)

where the second parameter is an object with properties corresponding to the publicly available properties on Atrament instances (according to the docs, that would include weight, color, smoothing, adaptiveStroke, mode and opacity) plus width and height (Even if I'd argue that you don't really need these last two. They're easily enough configured through the canvas element itself).

Sometimes drawing adds a line between beginning and end of stroke

I can't seem to figure out what is causing this, as it doesn't always happen. Sometimes when I draw on the canvas, when I let the mouse up, I get an extra straight line from the end point of the stroke to the beginning point of the stroke. I'm not 100% sure it's a bug. I could have implemented atrament wrong, but I can't seem to find it. Also, it's not saving that extra line to the stroke because if I hit "undo", it removes all of the extra lines from however many past strokes have had it.

Any help would be very appreciated. TIA

https://user-images.githubusercontent.com/39074364/114517894-639d7d80-9c04-11eb-9e13-1a591bbb74c8.mov
atramentfunctions.txt

Unable to draw on Android Chrome Browser

I have a canvas in a Bootstrap Modal.

Works perfectly well on a PC; Not working on Android phone

Used to work well until Chrome released a new version for Android. (on 18th Oct)

Even now, if canvas placed directly on form, it works. Doesn't work when placed in Modal.

Fill bucket doesn't work on mobile chrome

Steps to reproduce:

  • Open the demo at http://fiala.uk/atrament.js/demo/ on chrome mobile
  • Try to use the fill mode, either applied to the whole picture, or to a region delimited by a stroke. Nothing happens.

Tested on Samsung Galaxy S7 and Google Nexus 5 devices.

(As a side note, I couldn't get the demo working at all on Firefox Mobile)

Thank you

Test rendering with WebGL

This may produce smoother strokes and improve performance, but needs to be tested. I also don't want to do it if it means introducing a 3rd party rendering library.

"is dirty" check

It would be nice to have an indicator if anything has been drawn by the user yet. Of course needs to be reset to false when the user clears the canvas.

Clarify how to access Atrament class

It should be clarified that Atrament will never be exposed automatically. From the docs, I expected it to be in global scope, when including atrament.min.js.

Do not get me wrong though, I think it is good that there is only one global. This is just about clear documentation.

Demo link

The demo link no longer works. It goes to your homepage instead of the atrament demo.

Problems with hand-down on iPad with apple pencil

First off - great stuff. Thank you!

I am having trouble on using atrament.js on an iPad Pro with Apple Pencil. While writing the wrist-detection seems to have some difficulties during the way so a moosdown is simulated where my wrist is again and again. So when I write a sentence I get like 10 straight strokes on my canvas.

I guess some kind of test has to be run if the new mouseDown is done before an mouseUp is triggered and ignore that in that case.

Best
Jonas

unexpected token '('

i'm not sure where to look so I thought I would post it here. I am using atrament in my app and it works fine in chrome and firefox but in safari its giving me a strange error:

unexpected token '('

at this line:

set(x, y) {

in Point

any ideas?

chore: eslint

The code style is a bit messy and inconsistent here and there. Let's add eslint and implement a standard.

@jakubfiala feel free to assign this to me, I'll take it up sometime soon.

Undo/redo

Would really appreciate an undo/redo feature.

I guess that in order for this to work, we need to

  • save snapshots of the canvas, (example) or
  • save objects / user actions in an array and redraw canvas. (example)

Any thoughts? Any volunteers? πŸ˜ƒ

Realtime draw updates

strokerecorded event fires up only on mouseup. Is there an event that gives me real time draw data?

Do not reset canvas size to 500x500

IMHO, giving no width and height at all to atrament should not reset the dimensions of the canvas element. It is quite unexpected, not to say irritating.

In case you need the dimensions internally, you should instead read them from the canvas element.

Add a "disable" option for drawing mode.

It might be convenient to have a sketchpad open but disable drawing. For example, maybe some users are allowed to draw and others can see the drawing (via syncing the strokes in realtime) but those users should not be able to draw themselves. For example, that could be useful for a virtual blackboard. It'd be nice to set their sketchpad to "disabled" mode. I think this only requires adding the option of "disabled" and no extra code for it, since you're already checking mode on every stroke.

Single input is sometimes ignorned

If you want to draw a single dot, then sometime the input get's ignored. (maybe because mousemove doesnt trigger?) See the attachment for a screen grab.

input-eaten

Browsers affected: Chrome, Firefox, MS edge

Bundle with Rollup

v4 should be bundled with Rollup and only support modern browsers (definition of what that means TBA)

Stream the canvas as the drawing is made

Hi!

I’d like to stream the drawing as it’s being made to other clients. I’m looking for ways to do that.

One way would be to capture the stream of the canvas, and stream it via WebRTC.

Another would be to get real-time information of the drawing (points, colors, etc) and replay the movements on listener devices.

What would be your advice?
Thanks!

range input broken when using the library?

Hi,

I have no idea what is going on, but when I run this code, the range selector stops working in Chrome and Firefox (it works in Edge). By "not working" I mean that in Chrome you cannot deselect the slider, and in firefox you cannot move it. This is the code:

<html>
	<head>
		<script src="/assets/js/atrament.min.js"></script>
		<style>
			canvas {
				width: 200px;
				height: 200px;
				background-color: red;
			}
		</style>
	</head>
	<body>
		<canvas></canvas>
	</body>
	<script>
		var aaa = atrament('canvas', 200, 200);
	</script>
	<form>
		<input type="range"  min="0" max="10">
	</form>
</html>

And if I remove the atrament line, then it starts working:

<html>
	<head>
		<script src="/assets/js/atrament.min.js"></script>
		<style>
			canvas {
				width: 200px;
				height: 200px;
				background-color: red;
			}
		</style>
	</head>
	<body>
		<canvas></canvas>
	</body>
</html>

I think this issue does not ocurr with the older version of the library that you use in the demo.

with Touch, line is away from where finger presses

Great library, when I use it with mouse it's perfect. But when I use it with Touch, the line doesn't appear right where the finger presses, it appears below, but quite a bit below, which is very odd, why is the line not appearing right where the finger presses? I'm using Chrome browser in both Desktop PC and Android phone, same problem in both, latest versions of all

best wishes

Save strokes with uniform sizes

I'm not sure if this is a bug or a feature, but the canvas does not handle resizing on its own. In particular, because the strokes are saved with absolute x/y coordinates, you have to be careful when you repaint the canvas that the size of the original canvas was the same as the size of the current canvas. I suggest saving the points in strokes always to a canvas of a uniform (large) size, then having the repaint function (.draw or .beginStroke, etc.) adjust for the size of the canvas.

Again, this might be the intended behavior (and once I realized what was happening, it wasn't a big deal to fix it in my own project), but I think it would be a nice feature if an atrament sketchpad could auto-resize when the underlying canvas was resized.

Add support for advanced `Touch` properties

These include:

  • force - should slightly affect stroke weight
  • radiusX|Y and rotationAngle - should set stroke weight. I need to find a way to calculate the weight based on the ellipse shape and rotation, possibly adjusting it using the apparent stroke direction (vector based on previous stroke points)

MAYBE:

Add new mode

Hi,

Thank you for very nice library. I'm really excited to use it in my project.
Do you consider adding FILL as a third mode? I think it could be very useful

Thank,
Pavlo

When scrolling down the line is above the cursor

I can't really screenshot this, but wehen I'm scrolled up the line is right underneath my cursor, and when I scroll down the line is above my cursor.

Edit:
It only happens in device emulator mode.

Demo: Fill mode locks up browser (Chome & Firefox tested)

fillbroken

If I get some time I'll run some profiling to see what's happening. When you change to fill mode in Chrome it will lock up the browser. It's not real evident but in the gif after it locks (shows spinner) I'm trying to click the controls, slide the range inputs with no responsiveness.

Great little lib by the way πŸ‘

export as module

It doesn't look like this exports Atrament as a module. It would make it a lot easier to bundle it using tools like browserify or webpack.

Library should not add event listener on document

I mean this:

document.addEventListener('mouseup', mouseUp);

If something is happening outside of canvas (for example button clicking) it should not have any side effects for canvas, but it has currently. It's just a bad behaviour. Don't you agree?

question about layers/transparency

We're a group of students looking for a drawing library to use for a game we're making, and we love Atrament, but we need something that will save the drawing on a transparent background so we don't have the white square of canvas. I didn't know if there was a way to do that using Atrament, but thought I'd ask.

How to retain smoothing effect in SVG when programmatically creating SVG from stroke data

Hey @jakubfiala,

Thanks for the great library - definitely the most realistic canvas drawing package I've come across.

I am working with another developer to export the canvas data created by Atrament to an SVG. We've achieved this by listening to the "stroke recorded" event and using the returned data to re-draw the image onto a specialised canvas provided by https://github.com/gliffy/canvas2svg, which can then be exported as an SVG.

It all works well except that the final SVG does not have the smoothing effect applied. Therefore, it does not look anywhere as good as the Atrament canvas drawing.

We think we could fix this by forking canvas2svg and integrating the smoothing functions of Atrament... Would this work? Do you have any other suggestions as to how we can apply the smoothing effect to the programmatically re-created SVG?

If we can get it working we can also integrate it as an API method of Atrament if this is a feature you'd want. Thanks!

saving and opening a drawing

Hi,

I am not sure is this is a bug or not, but when I save a drawing, I cannot open it with the default image viewer of windows (Windows 10). However I can open it with MS paint and others. Therefore I wonder if there is something wrong with the generated image.

I get the image using the toimage() method and then I use python to save the generated image as:

with open("imageToSave2.png", "wb") as f:
    f.write(image_string[22:].decode('base64'))

I have followed this method to save the images generated by other JS drawing libraries, and I can open them with the windows image viewer.

Unable to save the canvas background color.

I am trying to save the canvas along with its background color and use it elsewhere to prepare a collage. But, I only get to see a black background. Any help to get this resolved greatly appreciated.
Regards

UncaughtReferenceError: 'Atrament' not defined

Hi,

Sorry if this is the wrong way to do it, I'm not used to Github issues.

I tried using your library, but I'm getting an UncaughtReferenceError: 'Atrament' not defined error and I'm not sure what I'm doing wrong.
I link it in < head > tag as your code saved in my own file, then I do the syntax from the documentation. Am I missing a dependency?

My repo is at https://keybase.pub/mgnica/website/ and the page using it is https://mgnica.keybase.pub/website/you.html for reference.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.