BitMappery

So you are rebuilding Photoshop in the browser ?

No, I'm building a tool that does the bare minimum what I require and what I don't find in other open source tools. That doesn't mean of course that contributions related to Photoshop-esque features aren't welcomed.

The Issue Tracker is your point of contact

Bug reports, feature requests, questions and discussions are welcome on the GitHub Issue Tracker, please do not send e-mails through the development website. However, please search before posting to avoid duplicates, and limit to one issue per post.

Please vote on feature requests by using the Thumbs Up/Down reaction on the first post.

All hand-written ?

Yep, though it helps having worked in the photo software industry for five years, having tackled the problems before. Also, BitMappery is reusing zCanvas under the hood for rendering and bitmap blitting. BitMappery is written on top of Vue using Vuex and VueI18n.

Model

BitMappery works with entities known as Documents. A Document contains several Layers, each of which define their content, transformation, Effects, etc. Each of the nested entity properties has its own factory (see @/src/factories/). The Document is managed by the Vuex document-module.js.

Document rendering and interactions

The Document is rendered one layer at a time onto a Canvas element, using zCanvas. Both the rendering and interaction handling is performed by @/src/components/ui/zcanvas/layer_sprite.js. Note that the purpose of the renderer is solely to delegate interactions events to the Layer entity. The renderer should represent the properties of the Layer, the Layer should never reverse-engineer from the onscreen content (especially as different window size and scaling factor will greatly complicate these matters when performed two-way).

Rendering transformations, text and effects is an asynchronous operation handled by @/src/services/render-service.js. The purpose of this service is to perform and cache repeated operations and eventually maintain the source bitmap represented by the layer-sprite.js.

State history

Mutations can be registered in state history (Vuex history-module.js) in order to provide undo and redo of operations. In order to prevent storing a lot of changes of the same property (for instance when dragging a slider), the storage of a new state is deferred through a queue. This is why history states are enqueued by propertyName:

When enqueuing a new state while there is an existing one enqueued for the same property name, the first state is updated so its redo will match that of the newest state, the undo remaining unchanged. The second state will not be added to the queue.

It is good to understand that the undo/redo for an action should be considered separate from the Vue component that is triggering the transaction, the reason being that the component can be unmounted at the moment the history state is changed (and the component is no longer active).

That's why undo/redo handlers should either work on variables in a local scope, or on the Vuex store when mutating store properties. When relying on store state and getters, be sure to cache their values in the local scope to avoid conflicts (for instance in below example we cache activeLayerIndex as it is used by the undo/redo methods to update a specific Layer. activeLayerIndex can change during the application lifetime before the undo/redo handler fires which would otherwise lead to the wrong Layer being updated.

update( propertyName, newValue ) {
    // cache the existing values of the property value we are about to mutate...
    const existingValue = this.getterForExistingValue;
    // ...and the layer index that is used to identify the layer containing the property
    const index = this.activeLayerIndex;
    const store = this.$store;
    // define the method that will mutate the existing value to given newValue
    const commit = () => store.commit( "updateLayer", { index, opts: { newValue } });
    // and perform the mutation directly
    commit();
    // now define and enqueue undo/redo handlers to reverse and redo the commit mutation
    enqueueState( propertyName, {
        undo() {
            store.commit( "updateLayerEffects", { index, opts: { existingValue } });
        },
        redo() {
            commit();
        },
    });
}

Dropbox integration

Requires you to register a client id or access token.

Project setup

npm install

Compiles and hot-reloads for development

npm run serve

Compiles and minifies for production

npm run build

Run your unit tests

npm run test

Lints and fixes files

npm run lint

TODO / Roadmap

  • Layer source and mask must not be stored as Vue observables
  • Implement history mechanism in more places, show state to undo/redo in application menu
  • Repeated presses on a clone stamp with source coords do not behave logically
  • Implement action queue when drawing, only execute drawing on zCanvas.sprite.update()-hook
  • Maintain cache for source images at the display destination size (invalidate on window resize / zoom), this prevents processing large images that are never displayed at their full scale
  • Dragging of masks on rotated/mirror content is kinda broken
  • Animate selection lines between white and black colors
  • Restored base64 images should be treated as binary once more (see layer-factory)
  • Zoom set original size isn't that accurate (check also on mobile views), needs calculateMaxScaling ?
  • Unload Blobs when images are no longer used in document (see sprite-factory disposeSprite, keep instance count of usages)
  • Implement layer sorting and opacity
  • Implement layer scaling
  • Implement merged layer selection
  • Scale logic should move from zoomable-canvas into zCanvas (as handleInteraction needs to transform offsets by zoom ratio, see LayerSprite!)
Languages
TypeScript 68.3%
Vue 28.8%
SCSS 2.1%
HTML 0.4%
C++ 0.3%
Other 0.1%