├── .prettierignore ├── docs ├── demo │ ├── .gitignore │ ├── tsconfig.json │ ├── build-config.json │ ├── icons │ │ ├── js-draw-icon-tiny.png │ │ ├── js-draw-icon-large.png │ │ ├── js-draw-icon-medium.png │ │ ├── js-draw-icon-small.png │ │ ├── js-draw-icon-vector-medium.svg │ │ └── js-draw-icon-vector-large.svg │ ├── ui │ │ ├── settingsDialog.css │ │ ├── newImageDialog.css │ │ ├── loadFromSaveList.css │ │ └── FloatingActionButton.css │ ├── README.md │ ├── types.ts │ ├── storage │ │ ├── ImageSaver.ts │ │ ├── makeReadOnlyStoreEntry.ts │ │ ├── makeFileSaver.ts │ │ └── AbstractStore.ts │ ├── example.html │ ├── package.json │ ├── sw.js │ ├── index.html │ └── manifest.json ├── debugging │ ├── undo-history-visualizer │ │ ├── README.md │ │ ├── tsconfig.json │ │ ├── build-config.json │ │ ├── package.json │ │ └── index.html │ ├── stroke-logging │ │ ├── README.md │ │ ├── tsconfig.json │ │ ├── build-config.json │ │ ├── package.json │ │ └── index.html │ ├── translation-tester │ │ ├── README.md │ │ ├── tsconfig.json │ │ ├── build-config.json │ │ └── package.json │ └── input-system-tester │ │ ├── tsconfig.json │ │ ├── build-config.json │ │ ├── README.md │ │ ├── package.json │ │ └── index.html ├── examples │ ├── example-collaborative │ │ ├── tsconfig.json │ │ ├── build-config.json │ │ ├── style.css │ │ ├── README.md │ │ └── package.json │ ├── example-custom-tools │ │ ├── tsconfig.json │ │ ├── build-config.json │ │ ├── README.md │ │ ├── package.json │ │ └── example.html │ ├── example-multiple-editors │ │ ├── tsconfig.json │ │ ├── build-config.json │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ └── example.ts │ └── example-save-restore-toolbar-state │ │ ├── tsconfig.json │ │ ├── build-config.json │ │ ├── README.md │ │ ├── package.json │ │ └── example.html ├── img │ └── readme-images │ │ ├── js-draw.jpg │ │ ├── js-draw.png │ │ └── unsupported-elements--in-editor.png ├── doc-pages │ ├── typedoc.json │ ├── README.md │ ├── inline-examples │ │ ├── README.txt │ │ ├── adding-a-stroke.md │ │ ├── editor-settings-polyline-pen.md │ │ ├── canvas-renderer.md │ │ └── adding-an-image-and-data-urls.md │ └── pages │ │ ├── guides │ │ └── components.md │ │ ├── migrations.md │ │ └── guides.md ├── index.html └── examples.md ├── testing ├── mocks │ ├── styleMock.js │ └── coloris.ts └── global.d.ts ├── packages ├── js-draw │ ├── .npmignore │ ├── src │ │ ├── image │ │ │ ├── lib.ts │ │ │ └── export │ │ │ │ └── adjustExportedSVGSize.ts │ │ ├── toolbar │ │ │ ├── constants.ts │ │ │ ├── widgets │ │ │ │ ├── SelectionToolWidget.scss │ │ │ │ ├── components │ │ │ │ │ ├── makeThicknessSlider.scss │ │ │ │ │ ├── components.scss │ │ │ │ │ ├── makeSeparator.scss │ │ │ │ │ ├── makeSeparator.ts │ │ │ │ │ ├── makeButtonGrid.scss │ │ │ │ │ ├── makeGridSelector.scss │ │ │ │ │ └── makeSnappedList.scss │ │ │ │ ├── HandToolWidget.scss │ │ │ │ ├── widgets.scss │ │ │ │ ├── DocumentPropertiesWidget.scss │ │ │ │ ├── PenToolWidget.scss │ │ │ │ ├── InsertImageWidget │ │ │ │ │ ├── fileToImages.ts │ │ │ │ │ └── InsertImageWidget.scss │ │ │ │ ├── lib.ts │ │ │ │ ├── OverflowWidget.css │ │ │ │ ├── TextToolWidget.test.ts │ │ │ │ ├── keybindings.ts │ │ │ │ ├── ExitActionWidget.ts │ │ │ │ └── SaveActionWidget.ts │ │ │ ├── toolbar.scss │ │ │ ├── lib.ts │ │ │ ├── types.ts │ │ │ ├── utils │ │ │ │ └── localization.ts │ │ │ ├── DropdownToolbar.test.ts │ │ │ └── DropdownToolbar.scss │ │ ├── testing │ │ │ ├── lib.ts │ │ │ ├── firstElementAncestorOfNode.ts │ │ │ ├── getUniquePointerId.ts │ │ │ ├── createEditor.ts │ │ │ ├── sendKeyPressRelease.ts │ │ │ ├── fillHtmlInput.ts │ │ │ ├── findNodeWithText.ts │ │ │ ├── sendHtmlSwipe.ts │ │ │ └── sendPenEvent.ts │ │ ├── tools │ │ │ ├── FindTool.css │ │ │ ├── tools.scss │ │ │ ├── SelectionTool │ │ │ │ ├── util │ │ │ │ │ └── makeClipboardErrorHandlers.scss │ │ │ │ ├── SelectAllShortcutHandler.ts │ │ │ │ ├── SelectionBuilders │ │ │ │ │ └── RectSelectionBuilder.ts │ │ │ │ └── types.ts │ │ │ ├── ToolEnabledGroup.ts │ │ │ ├── SoundUITool.scss │ │ │ ├── InputFilter │ │ │ │ ├── FunctionMapper.ts │ │ │ │ ├── InputPipeline.ts │ │ │ │ └── InputMapper.ts │ │ │ ├── UndoRedoShortcut.ts │ │ │ ├── lib.ts │ │ │ ├── util │ │ │ │ └── createMenuOverlay.test.ts │ │ │ ├── ToolSwitcherShortcut.ts │ │ │ ├── ToolbarShortcutHandler.ts │ │ │ └── ScrollbarTool.scss │ │ ├── shortcuts │ │ │ └── lib.ts │ │ ├── util │ │ │ ├── lib.ts │ │ │ ├── untilNextAnimationFrame.ts │ │ │ ├── waitForTimeout.ts │ │ │ ├── bytesToSizeString.test.ts │ │ │ ├── listPrefixMatch.ts │ │ │ ├── dom │ │ │ │ ├── createButton.ts │ │ │ │ ├── waitForImageLoaded.ts │ │ │ │ ├── addLongPressOrHoverCssClasses.ts │ │ │ │ ├── stopPropagationOfScrollingWheelEvents.ts │ │ │ │ └── cloneElementWithStyles.ts │ │ │ ├── bytesToSizeString.ts │ │ │ ├── waitForAll.ts │ │ │ ├── guessKeyCodeFromKey.ts │ │ │ ├── fileToBase64Url.test.ts │ │ │ └── mitLicenseAttribution.ts │ │ ├── styles.js │ │ ├── version.ts │ │ ├── bundle │ │ │ └── bundled.ts │ │ ├── localizations │ │ │ ├── en.ts │ │ │ ├── comments.ts │ │ │ └── getLocalizationTable.test.ts │ │ ├── commands │ │ │ ├── lib.ts │ │ │ └── UnresolvedCommand.ts │ │ ├── dialogs │ │ │ ├── makeAboutDialog.scss │ │ │ ├── dialogs.scss │ │ │ ├── makeMessageDialog.scss │ │ │ └── makeAboutDialog.ts │ │ ├── version.test.ts │ │ ├── components │ │ │ ├── builders │ │ │ │ ├── lib.ts │ │ │ │ ├── FreehandLineBuilder.test.ts │ │ │ │ └── types.ts │ │ │ ├── UnknownSVGObject.test.ts │ │ │ ├── util │ │ │ │ └── describeComponentList.ts │ │ │ ├── localization.ts │ │ │ ├── AbstractComponent.transformBy.test.ts │ │ │ └── lib.ts │ │ ├── rendering │ │ │ ├── lib.ts │ │ │ ├── localization.ts │ │ │ ├── caching │ │ │ │ ├── types.ts │ │ │ │ └── testUtils.ts │ │ │ └── renderers │ │ │ │ └── TextOnlyRenderer.test.ts │ │ ├── SVGLoader │ │ │ ├── SVGLoader.plugins.test.ts │ │ │ └── utils │ │ │ │ └── determineFontSize.ts │ │ ├── Editor.loadFrom.test.ts │ │ ├── Coloris.css │ │ └── UndoRedoHistory.test.ts │ ├── tsconfig.json │ ├── typedoc.json │ ├── .gitignore │ ├── dist-test │ │ └── test_imports │ │ │ ├── package.json │ │ │ ├── yarn.lock │ │ │ ├── dom-mocks.cjs │ │ │ ├── test-imports.js │ │ │ └── test-require.cjs │ ├── tools │ │ ├── allLocales.js │ │ └── prebuild.js │ ├── build-config.json │ └── LICENSE ├── math │ ├── src │ │ ├── rounding │ │ │ ├── lib.ts │ │ │ ├── constants.ts │ │ │ ├── cleanUpNumber.test.ts │ │ │ ├── getLenAfterDecimal.ts │ │ │ ├── cleanUpNumber.ts │ │ │ ├── toStringOfSamePrecision.test.ts │ │ │ └── toRoundedString.test.ts │ │ ├── Vec2.ts │ │ ├── shapes │ │ │ └── CubicBezier.ts │ │ ├── Vec2.test.ts │ │ └── polynomial │ │ │ ├── solveQuadratic.ts │ │ │ └── solveQuadratic.test.ts │ ├── README.md │ ├── build-config.json │ ├── tsconfig.json │ ├── typedoc.json │ ├── dist-test │ │ └── test_imports │ │ │ ├── package.json │ │ │ ├── yarn.lock │ │ │ ├── test-imports.js │ │ │ └── test-require.cjs │ ├── LICENSE │ └── package.json ├── debugging │ ├── build-config.json │ ├── tsconfig.json │ ├── src │ │ ├── lib.ts │ │ └── localization.ts │ ├── README.md │ ├── LICENSE │ └── package.json ├── material-icons │ ├── tsconfig.json │ ├── typedoc.json │ ├── src │ │ ├── bundle.ts │ │ ├── icons │ │ │ ├── Check.svg │ │ │ ├── LineWeight.svg │ │ │ ├── ExpandMore.svg │ │ │ ├── Title.svg │ │ │ ├── Close.svg │ │ │ ├── Edit.svg │ │ │ ├── ContentCopy.svg │ │ │ ├── ContentPaste.svg │ │ │ ├── Delete.svg │ │ │ ├── EditDocument.svg │ │ │ ├── InkPen.svg │ │ │ ├── Crop.svg │ │ │ ├── InkEraserOff.svg │ │ │ ├── Resize.svg │ │ │ ├── Undo.svg │ │ │ ├── Redo.svg │ │ │ ├── RotateLeft.svg │ │ │ ├── InkEraser.svg │ │ │ ├── CloudUpload.svg │ │ │ ├── Select.svg │ │ │ ├── LassoSelect.svg │ │ │ ├── Shapes.svg │ │ │ ├── InkHighlighter.svg │ │ │ ├── Draw.svg │ │ │ ├── Imagesmode.svg │ │ │ ├── PanTool.svg │ │ │ ├── TouchApp.svg │ │ │ └── ScreenLockRotation.svg │ │ ├── types.ts │ │ └── lib.ts │ ├── build-config.json │ └── package.json ├── build-tool │ ├── src │ │ ├── lib.ts │ │ ├── bundlerMocks │ │ │ └── module.ts │ │ ├── utils │ │ │ ├── regexEscape.ts │ │ │ └── forEachFileInDirectory.ts │ │ └── types.ts │ ├── tsconfig.json │ └── package.json └── typedoc-extensions │ ├── README.md │ ├── src │ ├── markdown │ │ └── htmlEscape.ts │ ├── browser │ │ ├── constants.ts │ │ ├── editor │ │ │ └── loadIframePreviewScript.ts │ │ └── iframe.scss │ └── CustomTheme.ts │ ├── tsconfig.json │ ├── build-config.json │ └── tools │ └── prebuild.js ├── .githooks └── pre-commit ├── .firebaserc ├── .git-blame-ignore-revs ├── .yarnrc.yml ├── .vscode └── settings.json ├── tsconfig.eslint.json ├── lerna.json ├── tsconfig-typedoc.json ├── tsconfig.mjs.json ├── lint-staged.config.js ├── .npmignore ├── tsdoc.json ├── .editorconfig ├── firebase.json ├── .prettierrc.json ├── typedoc.base.json ├── .github ├── pull_request_template.md ├── workflows │ ├── publish-to-npm.yml │ ├── run-tests-on-pull-request.yml │ └── firebase-hosting-pull-request.yml └── ISSUE_TEMPLATE │ └── feature_request.md ├── scripts ├── build-website.sh └── check-formatting-ci.sh ├── dist-test ├── icons-test.html └── editor-test.html ├── tsconfig.json ├── jest.config.js └── LICENSE /.prettierignore: -------------------------------------------------------------------------------- 1 | *.svg -------------------------------------------------------------------------------- /docs/demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /testing/mocks/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /packages/js-draw/.npmignore: -------------------------------------------------------------------------------- 1 | dist-test/ 2 | src/**/*.ts 3 | /tools/ -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | yarn run lint-staged 4 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "js-draw" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/debugging/undo-history-visualizer/README.md: -------------------------------------------------------------------------------- 1 | Shows the undo/redo stacks of an editor. 2 | -------------------------------------------------------------------------------- /packages/js-draw/src/image/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as EditorImage } from './EditorImage'; 2 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/constants.ts: -------------------------------------------------------------------------------- 1 | export const toolbarCSSPrefix = 'toolbar-'; 2 | -------------------------------------------------------------------------------- /packages/math/src/rounding/lib.ts: -------------------------------------------------------------------------------- 1 | export { toRoundedString } from './toRoundedString'; 2 | -------------------------------------------------------------------------------- /docs/debugging/stroke-logging/README.md: -------------------------------------------------------------------------------- 1 | This directory is intended to facilitate debugging. 2 | -------------------------------------------------------------------------------- /packages/math/src/rounding/constants.ts: -------------------------------------------------------------------------------- 1 | export const numberRegex = /^([-]?)(\d*)[.](\d+)$/; 2 | -------------------------------------------------------------------------------- /packages/math/README.md: -------------------------------------------------------------------------------- 1 | # `@js-draw/math` 2 | 3 | A library with math utilities used by `js-draw`. 4 | -------------------------------------------------------------------------------- /packages/math/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "inDirectory": "./src", 3 | "outDirectory": "./dist" 4 | } 5 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Formatted codebase with prettier 2 | b1312c21ae7a8bcce3a34e21bab68e2bb34f4a83 3 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | # Disable PnP -- it breaks build-tool's TypeScript compilation. 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /docs/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/debugging/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "inDirectory": "./src", 3 | "outDirectory": "./dist" 4 | } 5 | -------------------------------------------------------------------------------- /packages/math/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/debugging/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/js-draw/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "include": ["src/**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/debugging/translation-tester/README.md: -------------------------------------------------------------------------------- 1 | Allows testing translations created with the GitHub translation templates. 2 | -------------------------------------------------------------------------------- /packages/js-draw/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../typedoc.base.json"], 3 | "entryPoints": ["./src/lib.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/material-icons/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "include": ["src/**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "relative", 3 | "editor.insertSpaces": false 4 | } 5 | -------------------------------------------------------------------------------- /docs/debugging/stroke-logging/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/material-icons/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../typedoc.base.json"], 3 | "entryPoints": ["./src/lib.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/debugging/input-system-tester/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/debugging/translation-tester/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/examples/example-collaborative/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/examples/example-custom-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/img/readme-images/js-draw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalizedrefrigerator/js-draw/HEAD/docs/img/readme-images/js-draw.jpg -------------------------------------------------------------------------------- /docs/img/readme-images/js-draw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalizedrefrigerator/js-draw/HEAD/docs/img/readme-images/js-draw.png -------------------------------------------------------------------------------- /docs/debugging/undo-history-visualizer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/demo/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./index.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/demo/icons/js-draw-icon-tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalizedrefrigerator/js-draw/HEAD/docs/demo/icons/js-draw-icon-tiny.png -------------------------------------------------------------------------------- /docs/examples/example-multiple-editors/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/SelectionToolWidget.scss: -------------------------------------------------------------------------------- 1 | .selection-format-menu { 2 | &.disabled { 3 | opacity: 0.5; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/demo/icons/js-draw-icon-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalizedrefrigerator/js-draw/HEAD/docs/demo/icons/js-draw-icon-large.png -------------------------------------------------------------------------------- /docs/demo/icons/js-draw-icon-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalizedrefrigerator/js-draw/HEAD/docs/demo/icons/js-draw-icon-medium.png -------------------------------------------------------------------------------- /docs/demo/icons/js-draw-icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalizedrefrigerator/js-draw/HEAD/docs/demo/icons/js-draw-icon-small.png -------------------------------------------------------------------------------- /packages/js-draw/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | 4 | # Files automatically copied from the root of the repository 5 | README.md 6 | docs/ 7 | -------------------------------------------------------------------------------- /docs/examples/example-save-restore-toolbar-state/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | 4 | "include": ["**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/math/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../typedoc.base.json"], 3 | "entryPoints": ["./src/lib.ts"], 4 | "readme": "./README.md" 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./**/*.ts", "./**/*.tsx"], 4 | "exclude": ["./**/node_modules/"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/debugging/stroke-logging/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./index.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as sendPenEvent } from './sendPenEvent'; 2 | export { default as sendTouchEvent } from './sendTouchEvent'; 3 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/FindTool.css: -------------------------------------------------------------------------------- 1 | .find-tool-overlay { 2 | /* Show at the bottom of the screen. */ 3 | order: -1; 4 | 5 | position: absolute; 6 | } 7 | -------------------------------------------------------------------------------- /docs/debugging/input-system-tester/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./index.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/debugging/translation-tester/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./sandbox.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/doc-pages/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../typedoc.base.json"], 3 | "entryPoints": [], 4 | "projectDocuments": [], 5 | "includeVersion": false 6 | } 7 | -------------------------------------------------------------------------------- /docs/examples/example-collaborative/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./script.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/examples/example-custom-tools/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./example.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/material-icons/src/bundle.ts: -------------------------------------------------------------------------------- 1 | import makeMaterialIconProviderClass from './makeMaterialIconProvider'; 2 | 3 | export { makeMaterialIconProviderClass }; 4 | -------------------------------------------------------------------------------- /docs/debugging/undo-history-visualizer/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./index.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/examples/example-multiple-editors/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./example.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/examples/example-save-restore-toolbar-state/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundledFiles": [ 3 | { 4 | "name": "jsdraw", 5 | "inPath": "./example.ts" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/build-tool/src/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as BundledFile } from './BundledFile'; 2 | export { default as CompiledTypeScriptDirectory } from './CompiledTypeScriptDirectory'; 3 | -------------------------------------------------------------------------------- /packages/js-draw/src/shortcuts/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as KeyboardShortcutManager } from './KeyboardShortcutManager'; 2 | export { default as KeyBinding } from './KeyBinding'; 3 | -------------------------------------------------------------------------------- /docs/img/readme-images/unsupported-elements--in-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalizedrefrigerator/js-draw/HEAD/docs/img/readme-images/unsupported-elements--in-editor.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "1.32.0", 4 | "packages": ["docs/examples/*", "packages/*", "docs/demo", "docs/debugging/*"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/debugging/src/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as DebugToolbarWidget } from './DebugToolbarWidget'; 2 | export { default as playInputLog, LogEventRecord as InputLogEvent } from './playInputLog'; 3 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as adjustEditorThemeForContrast } from './adjustEditorThemeForContrast'; 2 | export { ReactiveValue, MutableReactiveValue } from './ReactiveValue'; 3 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig-typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "include": ["packages/**/*.ts", "docs/doc-pages/*.ts"], 5 | 6 | "exclude": ["packages/typedoc-extensions/", "**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.mjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "target": "ES6", 6 | "module": "ES6", 7 | "outDir": "dist/mjs" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,mjs,cjs,ts,tsx}': ['eslint --fix', 'prettier --write --ignore-unknown'], 3 | '*.{md,json,yml,scss,css,html}': ['prettier --write --ignore-unknown'], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/js-draw/src/styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Applies the editor's styles to the document. 3 | * @packageDocumentation 4 | */ 5 | 6 | import './Editor.scss'; 7 | import '@melloware/coloris/dist/coloris.css'; 8 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/components/makeThicknessSlider.scss: -------------------------------------------------------------------------------- 1 | .toolbar-thicknessSliderContainer { 2 | display: flex; 3 | flex-direction: row; 4 | 5 | input { 6 | flex-grow: 1; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/demo/ui/settingsDialog.css: -------------------------------------------------------------------------------- 1 | .settings-save-button { 2 | display: block; 3 | width: 80%; 4 | 5 | /* Center */ 6 | margin-left: auto; 7 | margin-right: auto; 8 | 9 | margin-top: 30px; 10 | } 11 | -------------------------------------------------------------------------------- /docs/examples/example-collaborative/style.css: -------------------------------------------------------------------------------- 1 | .js-draw.imageEditorContainer { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | width: 100vw; 8 | height: 100vh; 9 | } 10 | -------------------------------------------------------------------------------- /packages/build-tool/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "dist/" 6 | }, 7 | 8 | "include": ["**/*.ts"], 9 | "exclude": ["dist/"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/tools.scss: -------------------------------------------------------------------------------- 1 | @use './ScrollbarTool.scss'; 2 | @use './SelectionTool/SelectionTool.scss'; 3 | @use './FindTool.css'; 4 | @use './SoundUITool.scss'; 5 | @use './util/createMenuOverlay.scss'; 6 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/LineWeight.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/examples/example-multiple-editors/README.md: -------------------------------------------------------------------------------- 1 | # example-multiple-editors 2 | 3 | [index.html](./index.html) | [example.ts](./example.ts) 4 | 5 | This example adds multiple different editors to the same document. 6 | -------------------------------------------------------------------------------- /docs/examples/example-custom-tools/README.md: -------------------------------------------------------------------------------- 1 | # example-custom-tools 2 | 3 | [example.html](./example.html) | [example.ts](./example.ts) 4 | 5 | This example shows how to create custom tools and add them to the toolbar. 6 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/toolbar.scss: -------------------------------------------------------------------------------- 1 | @use './widgets/widgets.scss'; 2 | 3 | @use './AbstractToolbar.scss'; 4 | @use './EdgeToolbar.scss'; 5 | @use './DropdownToolbar.scss'; 6 | 7 | @use './utils/HelpDisplay.scss'; 8 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/ExpandMore.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Title.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/debugging/input-system-tester/README.md: -------------------------------------------------------------------------------- 1 | # input-system-tester 2 | 3 | Displays strokes processed by `StrokeSmoother` from preset input data. 4 | 5 | Use this project to evaluate changes to `StrokeSmoother.ts` and similar files. 6 | -------------------------------------------------------------------------------- /docs/demo/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | [index.html](./index.html) | [index.ts](./index.ts) 4 | 5 | This example shows how to use `js-draw`'s TypeScript API. 6 | 7 | ## Building 8 | 9 | Run `yarn build` or `yarn watch`. 10 | -------------------------------------------------------------------------------- /packages/build-tool/src/bundlerMocks/module.ts: -------------------------------------------------------------------------------- 1 | // Yarn patches TypeScript to include a `require("module")`. This breaks bundling with ESBuild. 2 | // To work around this, we remap the import to an empty file. 3 | export default {}; 4 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.bundle.js 2 | docs/ 3 | dist/docs/ 4 | dist/**/*.test.d.ts 5 | dist/**/*.test.js 6 | dist/testing/ 7 | dist-test/ 8 | src/**/*.ts 9 | .github/ 10 | .husky/ 11 | build_tools/ 12 | .vscode/ 13 | *.swp 14 | *.swo 15 | yarn-error.log -------------------------------------------------------------------------------- /tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "extends": ["typedoc/tsdoc.json"], 4 | "noStandardTags": false, 5 | "tagDefinitions": [{ "tagName": "@final", "syntaxKind": "modifier" }] 6 | } 7 | -------------------------------------------------------------------------------- /packages/js-draw/src/version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains the current version of the library -- used 3 | * internaly (e.g. for documentation). 4 | * @internal 5 | */ 6 | export default { 7 | // Note: Auto-updated by prebuild.js: 8 | number: '1.32.0', 9 | }; 10 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/README.md: -------------------------------------------------------------------------------- 1 | # `typedoc-extensions` 2 | 3 | A set of extensions that improve the `js-draw` documentation. 4 | 5 | Includes: 6 | 7 | - runnable examples 8 | - pre-rendered KaTeX 9 | - a workaround for a cross-package linking bug. 10 | -------------------------------------------------------------------------------- /packages/js-draw/src/bundle/bundled.ts: -------------------------------------------------------------------------------- 1 | // Main entrypoint for the bundler (ESBuild/Webpack/etc.) when creating the bundled 2 | // portion of a release. 3 | 4 | import '../styles'; 5 | import Editor from '../Editor'; 6 | export * from '../lib'; 7 | 8 | export default Editor; 9 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/untilNextAnimationFrame.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | const untilNextAnimationFrame = (): Promise => { 3 | return new Promise((resolve) => { 4 | requestAnimationFrame(() => resolve()); 5 | }); 6 | }; 7 | 8 | export default untilNextAnimationFrame; 9 | -------------------------------------------------------------------------------- /docs/doc-pages/README.md: -------------------------------------------------------------------------------- 1 | # doc-pages 2 | 3 | This folder includes Markdown documents that are rendered as a part of the documentation. 4 | 5 | `inline-examples/` includes files that can be `[include:...]`ed in other documents. 6 | 7 | `guides/` includes tutorials and migration guides. 8 | -------------------------------------------------------------------------------- /docs/examples/example-save-restore-toolbar-state/README.md: -------------------------------------------------------------------------------- 1 | # example-save-restore-tool-state 2 | 3 | [example.html](./example.html) | [example.ts](./example.ts) 4 | 5 | This example shows how to save and restore the state of the editor's toolbar, and by extension, the state of its tools. 6 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/src/markdown/htmlEscape.ts: -------------------------------------------------------------------------------- 1 | const htmlEscape = (text: string) => { 2 | return text 3 | .replace(/[&]/g, '&') 4 | .replace(/[<]/g, '<') 5 | .replace(/[>]/g, '>') 6 | .replace(/["]/g, '"'); 7 | }; 8 | 9 | export default htmlEscape; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # See https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*.{js,json,ts,html,py}] 6 | indent_style = tab 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{md,yml}] 13 | end_of_line = lf 14 | 15 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/components/components.scss: -------------------------------------------------------------------------------- 1 | @use './makeThicknessSlider.scss'; 2 | @use './makeColorInput.scss'; 3 | @use './makeSeparator.scss'; 4 | @use './makeFileInput.scss'; 5 | @use './makeGridSelector.scss'; 6 | @use './makeSnappedList.scss'; 7 | @use './makeButtonGrid.scss'; 8 | -------------------------------------------------------------------------------- /testing/mocks/coloris.ts: -------------------------------------------------------------------------------- 1 | // @melloware/coloris tries to use the HTML5Canvas on import, which is not supported by 2 | // JSDom (without installing additional packages). As such, we need to mock it. 3 | 4 | export const coloris = (_options: unknown): void => {}; 5 | 6 | export const init = () => {}; 7 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "docs", 4 | "ignore": [ 5 | "firebase.json", 6 | "src/**", 7 | ".husky/**", 8 | ".vscode/**", 9 | "build_tools/**", 10 | "dist/**", 11 | "__mocks__/**", 12 | "**/.*", 13 | "**/node_modules/**" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/js-draw/src/localizations/en.ts: -------------------------------------------------------------------------------- 1 | import { defaultEditorLocalization, EditorLocalization } from '../localization'; 2 | 3 | // Default localizations are already in English. 4 | const localization: EditorLocalization = { 5 | ...defaultEditorLocalization, 6 | }; 7 | 8 | export default localization; 9 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/lib.ts: -------------------------------------------------------------------------------- 1 | export * from './widgets/lib'; 2 | export * from './widgets/components/makeColorInput'; 3 | export { default as IconProvider, IconElemType } from './IconProvider'; 4 | export { makeDropdownToolbar } from './DropdownToolbar'; 5 | export { makeEdgeToolbar } from './EdgeToolbar'; 6 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/SelectionTool/util/makeClipboardErrorHandlers.scss: -------------------------------------------------------------------------------- 1 | .clipboard-error-dialog { 2 | details > summary { 3 | cursor: pointer; 4 | } 5 | 6 | details[open] { 7 | margin-bottom: 12px; 8 | } 9 | 10 | textarea { 11 | width: 100%; 12 | box-sizing: border-box; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/waitForTimeout.ts: -------------------------------------------------------------------------------- 1 | /** Returns a promise that resolves after `timeout` milliseconds. */ 2 | const waitForTimeout = (timeout: number): Promise => { 3 | return new Promise((resolve) => { 4 | setTimeout(() => resolve(), timeout); 5 | }); 6 | }; 7 | 8 | export default waitForTimeout; 9 | -------------------------------------------------------------------------------- /docs/demo/types.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from 'js-draw'; 2 | 3 | export type IconType = HTMLImageElement | SVGElement; 4 | 5 | type AppNotifierMessageType = 'image-saved'; 6 | type AppNotifierMessageValueType = null; 7 | export type AppNotifier = EventDispatcher; 8 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/HandToolWidget.scss: -------------------------------------------------------------------------------- 1 | .toolbar-zoomLevelEditor { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | } 6 | 7 | .toolbar-zoomLevelEditor .zoomDisplay { 8 | flex-grow: 1; 9 | } 10 | 11 | .toolbar-zoomLevelEditor button { 12 | min-width: 48px; 13 | } 14 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/widgets.scss: -------------------------------------------------------------------------------- 1 | @use './InsertImageWidget/InsertImageWidget.scss'; 2 | @use './OverflowWidget.css'; 3 | @use './PenToolWidget.scss'; 4 | @use './HandToolWidget.scss'; 5 | @use './SelectionToolWidget.scss'; 6 | @use './DocumentPropertiesWidget.scss'; 7 | @use './components/components.scss'; 8 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/material-icons/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "inDirectory": "./src", 3 | "outDirectory": "./dist", 4 | "prebuild": { "scriptPath": "./tools/prebuild.js" }, 5 | "bundledFiles": [ 6 | { 7 | "name": "jsdrawMaterialIcons", 8 | "inPath": "./src/bundle.ts", 9 | "outPath": "./dist/bundle.js" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/ContentCopy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/ContentPaste.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "jsxSingleQuote": true, 5 | "printWidth": 100, 6 | "useTabs": true, 7 | "bracketSameLine": true, 8 | "bracketSpacing": true, 9 | "overrides": [ 10 | { 11 | "files": "README.md", 12 | "options": { 13 | "useTabs": false 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/material-icons/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * All auto-generated icons should have this type. 3 | * 4 | * Goal: Ensure that icon processing functions are working with the original 5 | * auto-generated Material icons. 6 | * 7 | * @internal 8 | */ 9 | export interface OpaqueIconType { 10 | __opaqueType__iconType: unknown; 11 | } 12 | -------------------------------------------------------------------------------- /packages/math/src/Vec2.ts: -------------------------------------------------------------------------------- 1 | // Internally, we define Vec2 as a namespace within Vec3 -- 2 | // this allows referencing Vec2s from Vec3 constructors without 3 | // cyclic references. 4 | import { Vec3, Vec2 } from './Vec3'; 5 | 6 | export type Point2 = Vec3; 7 | export type Vec2 = Vec3; 8 | export { Vec3, Vec2 }; 9 | export default Vec2; 10 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | 6 | // See https://typedoc.org/api/modules/JSX.html 7 | "jsxFactory": "JSX.createElement", 8 | "jsxFragmentFactory": "JSX.Fragment" 9 | }, 10 | 11 | "include": ["**/*.ts", "**/*.tsx"] 12 | } 13 | -------------------------------------------------------------------------------- /docs/doc-pages/inline-examples/README.txt: -------------------------------------------------------------------------------- 1 | These examples can be included in the [js-draw documentation](https://personalizedrefrigerator.github.io/js-draw/typedoc/) 2 | with include tags like 3 | ``` 4 | [[include:doc-pages/inline-examples/settings-example.md]] 5 | ``` 6 | 7 | Keeping longer examples in this directory helps prevent the main code directory from becoming cluttered. -------------------------------------------------------------------------------- /packages/build-tool/src/utils/regexEscape.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Escapes `text` for use in a regular expression. WARNING: May not escape all control characters. 3 | * 4 | * **Note**: When the built-in RegExp.escape works in NodeJS, this should be replaced. 5 | */ 6 | const regexEscape = (text: string) => text.replace(/[$^\\.*+?()[\]{}|/]/g, '\\$&'); 7 | export default regexEscape; 8 | -------------------------------------------------------------------------------- /packages/js-draw/dist-test/test_imports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-draw-test-imports", 3 | "version": "0.0.1", 4 | "description": "Test module and CommonJS imports", 5 | "author": "Henry Heino", 6 | "license": "MIT", 7 | "private": true, 8 | "type": "module", 9 | "scripts": { 10 | "test": "node test-imports.js && node test-require.cjs" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/math/dist-test/test_imports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-draw-math-test-imports", 3 | "version": "0.0.1", 4 | "description": "Test module and CommonJS imports", 5 | "author": "Henry Heino", 6 | "license": "MIT", 7 | "private": true, 8 | "type": "module", 9 | "scripts": { 10 | "test": "node test-imports.js && node test-require.cjs" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/debugging/README.md: -------------------------------------------------------------------------------- 1 | # `@js-draw/debugging` 2 | 3 | A library with a set of debugging/development tools for `js-draw`. 4 | 5 | This tool makes use of a number of unstable APIs within `js-draw`. Some of these APIs may make `js-draw` unstable or even have effects on **other editors running in the same page**. 6 | 7 | Avoid using this tool for anything other than debugging. 8 | -------------------------------------------------------------------------------- /docs/doc-pages/inline-examples/adding-a-stroke.md: -------------------------------------------------------------------------------- 1 | ```ts,runnable 2 | import { 3 | Editor, EditorImage, Stroke, Path, Color4, 4 | } from 'js-draw'; 5 | 6 | const editor = new Editor(document.body); 7 | 8 | const stroke = Stroke.fromFilled( 9 | Path.fromString('m0,0 l100,100 l0,-10 z'), 10 | Color4.red, 11 | ); 12 | editor.dispatch(EditorImage.addComponent(stroke)); 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/doc-pages/pages/guides/components.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Components 3 | category: Guides 4 | children: 5 | - ./components/strokes.md 6 | - ./components/custom-components.md 7 | --- 8 | 9 | # Image components 10 | 11 | The guides in this section show: 12 | 13 | - How to change the content of an open editor (erase/duplicate/add strokes). 14 | - How to create custom components. 15 | -------------------------------------------------------------------------------- /packages/js-draw/src/commands/lib.ts: -------------------------------------------------------------------------------- 1 | import Command from './Command'; 2 | import Duplicate from './Duplicate'; 3 | import Erase from './Erase'; 4 | import invertCommand from './invertCommand'; 5 | import SerializableCommand from './SerializableCommand'; 6 | import uniteCommands from './uniteCommands'; 7 | 8 | export { Command, Duplicate, Erase, SerializableCommand, invertCommand, uniteCommands }; 9 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/EditDocument.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/InkPen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/src/browser/constants.ts: -------------------------------------------------------------------------------- 1 | interface ExtendedWindow extends Window { 2 | basePath: string; 3 | assetsURL: string; 4 | imagesURL: string; 5 | } 6 | 7 | declare const window: ExtendedWindow; 8 | 9 | export const basePath: string = window.basePath; 10 | export const assetsPath: string = window.assetsURL; 11 | export const imagesPath: string = window.imagesURL; 12 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/types.ts: -------------------------------------------------------------------------------- 1 | import IconProvider from './IconProvider'; 2 | import type { ToolbarLocalization } from './localization'; 3 | 4 | export interface ActionButtonIcon { 5 | icon: Element; 6 | label: string; 7 | } 8 | 9 | export interface ToolbarContext { 10 | announceForAccessibility(text: string): void; 11 | localization: ToolbarLocalization; 12 | icons: IconProvider; 13 | } 14 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Crop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/DocumentPropertiesWidget.scss: -------------------------------------------------------------------------------- 1 | .toolbar-document-properties-widget { 2 | button.about-button { 3 | width: 100%; 4 | text-align: end; 5 | } 6 | 7 | & > * { 8 | --align-items-to-x: 120px; 9 | } 10 | 11 | .js-draw-size-input-row.js-draw-size-input-row { 12 | display: flex; 13 | 14 | &.size-input-row--automatic-size { 15 | display: none; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/InkEraserOff.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Resize.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/js-draw/dist-test/test_imports/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 8 6 | cacheKey: 10c0 7 | 8 | "js-draw-test-imports@workspace:.": 9 | version: 0.0.0-use.local 10 | resolution: "js-draw-test-imports@workspace:." 11 | languageName: unknown 12 | linkType: soft 13 | -------------------------------------------------------------------------------- /packages/js-draw/tools/allLocales.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | const allLocales = require('../dist/cjs/localizations/getLocalizationTable').allLocales; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-require-imports 5 | const comments = require('../dist/cjs/localizations/comments').default; 6 | 7 | module.exports = { 8 | locales: allLocales, 9 | comments, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/math/dist-test/test_imports/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 8 6 | cacheKey: 10c0 7 | 8 | "js-draw-math-test-imports@workspace:.": 9 | version: 0.0.0-use.local 10 | resolution: "js-draw-math-test-imports@workspace:." 11 | languageName: unknown 12 | linkType: soft 13 | -------------------------------------------------------------------------------- /docs/demo/storage/ImageSaver.ts: -------------------------------------------------------------------------------- 1 | // Represents a method of saving an image (e.g. to localStorage). 2 | interface ImageSaver { 3 | // Returns a message describing whether the image was saved 4 | write(svgData: string): Promise; 5 | 6 | title: string; 7 | 8 | updatePreview: ((newPreviewData: string) => Promise) | null; 9 | updateTitle: ((newTitle: string) => Promise) | null; 10 | } 11 | 12 | export default ImageSaver; 13 | -------------------------------------------------------------------------------- /packages/math/dist-test/test_imports/test-imports.js: -------------------------------------------------------------------------------- 1 | console.log('Testing imports...'); 2 | 3 | import { Vec2, Color4, Mat33 } from '@js-draw/math'; 4 | 5 | if (Vec2.of(1, 1).x !== 1) { 6 | throw new Error('Failed to import module Vec2'); 7 | } 8 | 9 | if (!Mat33.identity) { 10 | throw new Error('Failed to import Mat33 via CommonJS'); 11 | } 12 | 13 | if (!Color4.red) { 14 | throw new Error('Failed to import Color4 from js-draw'); 15 | } 16 | -------------------------------------------------------------------------------- /typedoc.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["**/*.test.ts", "node_modules/**", "**/dist/**", "**/dist-test/**", "src/testing/"], 3 | "excludePrivate": true, 4 | "excludeInternal": true, 5 | "includeVersion": true, 6 | "commentStyle": "all", 7 | "entryPointStrategy": "resolve", 8 | "tsconfig": "tsconfig-typedoc.json", 9 | 10 | "validation": { 11 | "notExported": false, 12 | "invalidLink": true, 13 | "notDocumented": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/js-draw/src/dialogs/makeAboutDialog.scss: -------------------------------------------------------------------------------- 1 | .about-dialog-content > .scroll { 2 | white-space: pre-wrap; 3 | font-family: monospace; 4 | 5 | & > details > summary { 6 | cursor: pointer; 7 | } 8 | 9 | & > h2, 10 | & > details > summary { 11 | margin-top: 15px; 12 | 13 | font-size: 1.2em; 14 | font-weight: bold; 15 | 16 | a { 17 | color: var(--foreground-color-1); 18 | text-decoration: underline; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/bytesToSizeString.test.ts: -------------------------------------------------------------------------------- 1 | import bytesToSizeString from './bytesToSizeString'; 2 | 3 | describe('bytesToSizeString', () => { 4 | test.each([ 5 | [1024, { size: 1, units: 'KiB' }], 6 | [1024 * 1024, { size: 1, units: 'MiB' }], 7 | [256, { size: 256, units: 'B' }], 8 | ])('should correctly assign units for different byte values', (size, expected) => { 9 | expect(bytesToSizeString(size)).toMatchObject(expected); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/ToolEnabledGroup.ts: -------------------------------------------------------------------------------- 1 | import BaseTool from './BaseTool'; 2 | 3 | // Connects a group of tools -- at most one tool in the group can be enabled. 4 | export default class ToolEnabledGroup { 5 | private activeTool: BaseTool | null; 6 | public constructor() {} 7 | 8 | public notifyEnabled(tool: BaseTool) { 9 | if (tool !== this.activeTool) { 10 | this.activeTool?.setEnabled(false); 11 | this.activeTool = tool; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/firstElementAncestorOfNode.ts: -------------------------------------------------------------------------------- 1 | /** Returns the first ancestor of the given node (or the node itself) that is an HTMLElement */ 2 | const firstElementAncestorOfNode = (node: Node | null): HTMLElement | null => { 3 | if (node instanceof HTMLElement) { 4 | return node; 5 | } else if (node?.parentNode) { 6 | return firstElementAncestorOfNode(node.parentNode); 7 | } 8 | return null; 9 | }; 10 | 11 | export default firstElementAncestorOfNode; 12 | -------------------------------------------------------------------------------- /packages/js-draw/src/version.test.ts: -------------------------------------------------------------------------------- 1 | import version from './version'; 2 | import { readFile } from 'node:fs/promises'; 3 | import { join, dirname } from 'path'; 4 | 5 | describe('version', () => { 6 | it('version should be correct', async () => { 7 | const packageJSONString = await readFile(join(dirname(__dirname), 'package.json'), 'utf-8'); 8 | const packageJSON = JSON.parse(packageJSONString); 9 | 10 | expect(packageJSON['version']).toBe(version.number); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/examples/example-collaborative/README.md: -------------------------------------------------------------------------------- 1 | # example-collaborative 2 | 3 | [script.ts](./script.ts) | [server.py](./server.py) 4 | 5 | This example shows how to serialize `js-draw` commands (e.g. `AddElementCommand`), send them to a server, and deserialize them. 6 | 7 | > [!NOTE] 8 | > 9 | > Serialization/deserialization of `Command`s is **not** as well tested as other parts of `js-draw`. If you encounter bugs, [please report them](https://github.com/personalizedrefrigerator/js-draw/issues). 10 | -------------------------------------------------------------------------------- /packages/js-draw/dist-test/test_imports/dom-mocks.cjs: -------------------------------------------------------------------------------- 1 | // Note: Despite not using imports/exports, this file needs to be .cjs 2 | // to work around a require() issue. 3 | 4 | // TODO: This is added to support importing Editor despite its dependency on the 5 | // coloris color picker. Remove these mocks after switching to a different color picker. 6 | global.window = { addEventListener: () => {} }; 7 | global.document = { createElement: () => ({ getContext: () => null }) }; 8 | global.NodeList = undefined; 9 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/components/makeSeparator.scss: -------------------------------------------------------------------------------- 1 | .tool-dropdown-separator { 2 | // Default border color if color-mix is not supported. 3 | --border-color: rgba(100, 100, 100, 0.2); 4 | 5 | // A mostly-transparent version of the foreground color. 6 | --border-color: color-mix(in srgb, var(--foreground-color-1), rgba(0, 0, 0, 0) 80%); 7 | 8 | border-top: 1px solid var(--border-color); 9 | 10 | padding-left: 2px; 11 | 12 | margin-top: 10px; 13 | margin-bottom: 10px; 14 | } 15 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/RotateLeft.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/demo/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redirecting... 5 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/InkEraser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/listPrefixMatch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true iff all elements in the shorter list equal (===) the elements 3 | * in the longer list. 4 | */ 5 | const listPrefixMatch = (a: T[], b: T[]) => { 6 | const shorter = a.length < b.length ? a : b; 7 | const longer = shorter === a ? b : a; 8 | 9 | for (let i = 0; i < shorter.length; i++) { 10 | if (shorter[i] !== longer[i]) { 11 | return false; 12 | } 13 | } 14 | 15 | return true; 16 | }; 17 | 18 | export default listPrefixMatch; 19 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/components/makeSeparator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a separator element that renders a line and, optionally, a header. 3 | */ 4 | const makeSeparator = (header: string = '') => { 5 | const container = document.createElement('div'); 6 | container.classList.add('tool-dropdown-separator'); 7 | container.innerText = header; 8 | 9 | return { 10 | addTo: (parent: HTMLElement) => { 11 | parent.appendChild(container); 12 | }, 13 | }; 14 | }; 15 | 16 | export default makeSeparator; 17 | -------------------------------------------------------------------------------- /docs/demo/icons/js-draw-icon-vector-medium.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | Redirecting to /demo/index.html... 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/js-draw/src/components/builders/lib.ts: -------------------------------------------------------------------------------- 1 | export { makeFreehandLineBuilder } from './FreehandLineBuilder'; 2 | export { makePolylineBuilder } from './PolylineBuilder'; 3 | export { makePressureSensitiveFreehandLineBuilder } from './PressureSensitiveFreehandLineBuilder'; 4 | export { makeOutlinedCircleBuilder } from './CircleBuilder'; 5 | export { makeArrowBuilder } from './ArrowBuilder'; 6 | export { makeLineBuilder } from './LineBuilder'; 7 | export { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from './RectangleBuilder'; 8 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/getUniquePointerId.ts: -------------------------------------------------------------------------------- 1 | import Pointer from '../Pointer'; 2 | 3 | /** Returns the smallest ID not used by the pointers in the given list. */ 4 | const getUniquePointerId = (pointers: Pointer[]) => { 5 | let ptrId = 0; 6 | 7 | const pointerIds = pointers.map((ptr) => ptr.id); 8 | pointerIds.sort(); 9 | for (const pointerId of pointerIds) { 10 | if (ptrId === pointerId) { 11 | ptrId = pointerId + 1; 12 | } 13 | } 14 | 15 | return ptrId; 16 | }; 17 | 18 | export default getUniquePointerId; 19 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/CloudUpload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "inDirectory": "./src", 3 | "outDirectory": "./dist", 4 | 5 | "bundledFiles": [ 6 | { 7 | "name": "browser", 8 | "inPath": "./src/browser/browser.ts", 9 | "outPath": "./dist/js-draw-typedoc-extension--browser.js" 10 | }, 11 | 12 | { 13 | "name": "preview", 14 | "inPath": "./src/browser/iframe-preview-setup.ts", 15 | "outPath": "./dist/js-draw-typedoc-extension--iframe.js" 16 | } 17 | ], 18 | "prebuild": { "scriptPath": "./tools/prebuild.js" } 19 | } 20 | -------------------------------------------------------------------------------- /packages/js-draw/src/components/UnknownSVGObject.test.ts: -------------------------------------------------------------------------------- 1 | import AbstractComponent from './AbstractComponent'; 2 | import UnknownSVGObject from './UnknownSVGObject'; 3 | 4 | describe('UnknownSVGObject', () => { 5 | it('should not be deserializable', () => { 6 | const obj = new UnknownSVGObject( 7 | document.createElementNS('http://www.w3.org/2000/svg', 'circle'), 8 | ); 9 | const serialized = obj.serialize(); 10 | expect(() => AbstractComponent.deserialize(serialized)).toThrow(/.*cannot be deserialized.*/); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/SoundUITool.scss: -------------------------------------------------------------------------------- 1 | .js-draw-sound-ui-toggle { 2 | width: 0px; 3 | height: 0px; 4 | overflow: hidden; 5 | 6 | user-select: none; 7 | -webkit-user-select: none; 8 | } 9 | 10 | .js-draw-sound-ui-toggle button { 11 | margin-top: 1px; 12 | } 13 | 14 | .js-draw-sound-ui-toggle:focus-within, 15 | .js-draw-sound-ui-toggle.sound-ui-tool-enabled { 16 | overflow: visible; 17 | z-index: 5; 18 | } 19 | 20 | .js-draw-sound-ui-toggle:not(:focus-within):not(:hover).sound-ui-tool-enabled { 21 | opacity: 0.5; 22 | } 23 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Select.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/math/dist-test/test_imports/test-require.cjs: -------------------------------------------------------------------------------- 1 | console.log('Testing require()...'); 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-require-imports -- This is a .cjs file 4 | const { Vec2, Color4, Mat33 } = require('@js-draw/math'); 5 | 6 | if (Vec2.of(1, 1).x !== 1) { 7 | throw new Error('Failed to import module Vec2'); 8 | } 9 | 10 | if (!Mat33.identity) { 11 | throw new Error('Failed to import Mat33 via CommonJS'); 12 | } 13 | 14 | if (!Color4.red) { 15 | throw new Error('Failed to import Color4 from js-draw'); 16 | } 17 | -------------------------------------------------------------------------------- /packages/js-draw/dist-test/test_imports/test-imports.js: -------------------------------------------------------------------------------- 1 | console.log('Testing imports...'); 2 | 3 | import { TextComponent, StrokeComponent } from 'js-draw/components'; 4 | import './dom-mocks.cjs'; 5 | import { Editor } from 'js-draw/Editor'; 6 | 7 | if (!Editor) { 8 | throw new Error('Failed to import Editor'); 9 | } 10 | 11 | if (!TextComponent.fromLines) { 12 | throw new Error('Failed to import module TextComponent'); 13 | } 14 | 15 | if (!StrokeComponent.deserializeFromJSON) { 16 | throw new Error('Failed to import StrokeComponent'); 17 | } 18 | -------------------------------------------------------------------------------- /docs/examples/example-collaborative/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/example-collaborative", 3 | "version": "1.32.0", 4 | "description": "Example collaborative editor", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "bundle": "ts-node scripts/bundle.ts", 9 | "watchBundle": "ts-node scripts/watchBundle.ts", 10 | "build": "build-tool build", 11 | "watch": "build-tool watch" 12 | }, 13 | "dependencies": { 14 | "js-draw": "^1.32.0" 15 | }, 16 | "devDependencies": { 17 | "@js-draw/build-tool": "^1.32.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/InputFilter/FunctionMapper.ts: -------------------------------------------------------------------------------- 1 | import { InputEvt } from '../../inputEvents'; 2 | import InputMapper from './InputMapper'; 3 | 4 | /** 5 | * An `InputMapper` that applies a function to all events it receives. 6 | * 7 | * Useful for automated testing. 8 | */ 9 | export default class FunctionMapper extends InputMapper { 10 | public constructor(private fn: (event: InputEvt) => InputEvt) { 11 | super(); 12 | } 13 | 14 | public override onEvent(event: InputEvt): boolean { 15 | return this.emit(this.fn(event)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/debugging/undo-history-visualizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/debug-history-visualizer", 3 | "version": "1.32.0", 4 | "description": "Allows viewing undo history", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "bundle": "ts-node scripts/bundle.ts", 9 | "watchBundle": "ts-node scripts/watchBundle.ts", 10 | "build": "build-tool build", 11 | "watch": "build-tool watch" 12 | }, 13 | "dependencies": { 14 | "js-draw": "^1.32.0" 15 | }, 16 | "devDependencies": { 17 | "@js-draw/build-tool": "^1.32.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/examples/example-custom-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/example-custom-tools", 3 | "version": "1.32.0", 4 | "description": "Example demonstrating custom tools", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "bundle": "ts-node scripts/bundle.ts", 9 | "watchBundle": "ts-node scripts/watchBundle.ts", 10 | "build": "build-tool build", 11 | "watch": "build-tool watch" 12 | }, 13 | "dependencies": { 14 | "js-draw": "^1.32.0" 15 | }, 16 | "devDependencies": { 17 | "@js-draw/build-tool": "^1.32.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/LassoSelect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/doc-pages/inline-examples/editor-settings-polyline-pen.md: -------------------------------------------------------------------------------- 1 | ```ts,runnable 2 | import { Editor, makePolylineBuilder } from 'js-draw'; 3 | 4 | const editor = new Editor(document.body, { 5 | pens: { 6 | additionalPenTypes: [{ 7 | name: 'Polyline (For debugging)', 8 | id: 'custom-polyline', 9 | factory: makePolylineBuilder, 10 | 11 | // The pen doesn't create fixed shapes (e.g. squares, rectangles, etc) 12 | // and so should go under the "pens" section. 13 | isShapeBuilder: false, 14 | }], 15 | }, 16 | }); 17 | editor.addToolbar(); 18 | ``` 19 | -------------------------------------------------------------------------------- /packages/debugging/src/localization.ts: -------------------------------------------------------------------------------- 1 | import { matchingLocalizationTable } from 'js-draw'; 2 | 3 | export interface Localization { 4 | debugWidgetTitle: string; 5 | } 6 | 7 | export const defaultLocalizations: Localization = { 8 | debugWidgetTitle: 'Debug', 9 | }; 10 | 11 | export const localizationTables: Record = { 12 | en: { 13 | ...defaultLocalizations, 14 | }, 15 | }; 16 | 17 | export const getLocalizationTable = () => { 18 | return matchingLocalizationTable(navigator.languages, localizationTables, defaultLocalizations); 19 | }; 20 | -------------------------------------------------------------------------------- /docs/examples/example-multiple-editors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Multiple Editors | js-draw examples 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/js-draw/src/localizations/comments.ts: -------------------------------------------------------------------------------- 1 | import { EditorLocalization } from '../localization'; 2 | 3 | /** 4 | * Comments to help translators create translations. 5 | * 6 | * The key for each comment should be the same as is used in the 7 | * translation and original source records. 8 | */ 9 | const comments: Partial> = { 10 | pen: 'Likely unused', 11 | dragAndDropHereOrBrowse: 'Uses {{curly braces}} to denote bold text', 12 | closeSidebar: 'Currently used as an accessibilty label', 13 | }; 14 | 15 | export default comments; 16 | -------------------------------------------------------------------------------- /docs/examples/example-custom-tools/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom Tools | js-draw examples 7 | 19 | 20 | 21 | Loading... 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/demo", 3 | "version": "1.32.0", 4 | "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ", 5 | "author": "Henry Heino", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "build": "build-tool build", 10 | "watch": "build-tool watch" 11 | }, 12 | "dependencies": { 13 | "@js-draw/debugging": "^1.32.0", 14 | "@js-draw/material-icons": "^1.32.0", 15 | "js-draw": "^1.32.0" 16 | }, 17 | "devDependencies": { 18 | "@js-draw/build-tool": "^1.32.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/components/makeButtonGrid.scss: -------------------------------------------------------------------------------- 1 | .toolbar-button-grid { 2 | display: grid; 3 | grid-template-columns: repeat(var(--column-count), 1fr); 4 | justify-items: center; 5 | --button-size: 30px; 6 | 7 | > .button { 8 | font-size: 1em; 9 | width: min-content; 10 | 11 | > .icon { 12 | max-width: var(--button-size); 13 | max-height: var(--button-size); 14 | 15 | // Ensures that all icons have the same base size 16 | width: 48px; 17 | height: 48px; 18 | } 19 | 20 | > label { 21 | display: block; 22 | font-weight: normal; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/examples/example-save-restore-toolbar-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/example-save-restore-toolbar-state", 3 | "version": "1.32.0", 4 | "description": "Example demonstrating saving and restoring toolbar state", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "bundle": "ts-node scripts/bundle.ts", 9 | "watchBundle": "ts-node scripts/watchBundle.ts", 10 | "build": "build-tool build", 11 | "watch": "build-tool watch" 12 | }, 13 | "dependencies": { 14 | "js-draw": "^1.32.0" 15 | }, 16 | "devDependencies": { 17 | "@js-draw/build-tool": "^1.32.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | 4 | 5 | # Testing 6 | 7 | 10 | 11 | # Screenshots/other information 12 | 13 | 16 | 17 | Resolves #[issue number here]. 18 | -------------------------------------------------------------------------------- /docs/debugging/translation-tester/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/translation-tester", 3 | "version": "1.32.0", 4 | "description": "Allows testing a translation from a GitHub issue", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "bundle": "ts-node scripts/bundle.ts", 9 | "watchBundle": "ts-node scripts/watchBundle.ts", 10 | "build": "build-tool build", 11 | "watch": "build-tool watch" 12 | }, 13 | "dependencies": { 14 | "@js-draw/material-icons": "^1.32.0", 15 | "js-draw": "^1.32.0" 16 | }, 17 | "devDependencies": { 18 | "@js-draw/build-tool": "^1.32.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/examples/example-save-restore-toolbar-state/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Saving toolbar state | js-draw examples 7 | 19 | 20 | 21 | Loading... 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/build-tool/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/build-tool", 3 | "version": "1.32.0", 4 | "description": "Tool to facilitate building and bundling TypeScript for js-draw.", 5 | "license": "MIT", 6 | "private": true, 7 | "main": "./src/lib.ts", 8 | "bin": "./dist/main.js", 9 | "scripts": { 10 | "postinstall": "yarn run build", 11 | "build": "tsc" 12 | }, 13 | "dependencies": { 14 | "esbuild": "0.25.0", 15 | "path-browserify": "1.0.1", 16 | "sass": "1.70.0", 17 | "ts-node": "10.9.2", 18 | "typescript": "5.7.2" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "20.8.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/debugging/stroke-logging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/debug-stroke-logging", 3 | "version": "1.32.0", 4 | "description": "Allows sending logs", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "bundle": "ts-node scripts/bundle.ts", 9 | "watchBundle": "ts-node scripts/watchBundle.ts", 10 | "build": "build-tool build", 11 | "watch": "build-tool watch" 12 | }, 13 | "dependencies": { 14 | "@js-draw/debugging": "^1.32.0", 15 | "@js-draw/material-icons": "^1.32.0", 16 | "js-draw": "^1.32.0" 17 | }, 18 | "devDependencies": { 19 | "@js-draw/build-tool": "^1.32.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/examples/example-multiple-editors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/example-multiple-editors", 3 | "version": "1.32.0", 4 | "description": "Example demonstrating multiple editors in the same document", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "bundle": "ts-node scripts/bundle.ts", 9 | "watchBundle": "ts-node scripts/watchBundle.ts", 10 | "build": "build-tool build", 11 | "watch": "build-tool watch" 12 | }, 13 | "dependencies": { 14 | "@js-draw/material-icons": "^1.32.0", 15 | "js-draw": "^1.32.0" 16 | }, 17 | "devDependencies": { 18 | "@js-draw/build-tool": "^1.32.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Shapes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/js-draw/build-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "inDirectory": "./src", 3 | "outDirectory": "./dist", 4 | "scssFiles": ["./src/Editor.scss"], 5 | "bundledFiles": [ 6 | { 7 | "name": "jsdraw", 8 | "inPath": "./src/bundle/bundled.ts", 9 | "outPath": "./dist/bundle.js" 10 | }, 11 | { 12 | "name": "jsdrawStyles", 13 | "inPath": "./src/styles.js", 14 | "outPath": "./dist/bundledStyles.js" 15 | } 16 | ], 17 | "translationSourceFiles": [ 18 | { 19 | "name": "js-draw", 20 | "path": "./tools/allLocales.js", 21 | "defaultLocale": "en" 22 | } 23 | ], 24 | "prebuild": { "scriptPath": "./tools/prebuild.js" } 25 | } 26 | -------------------------------------------------------------------------------- /packages/js-draw/src/dialogs/dialogs.scss: -------------------------------------------------------------------------------- 1 | @use './makeAboutDialog.scss'; 2 | @use './makeMessageDialog.scss'; 3 | 4 | .dialog-container { 5 | > dialog { 6 | background-color: var(--background-color-1); 7 | color: var(--foreground-color-1); 8 | border: none; 9 | outline: none; 10 | 11 | box-shadow: 0 0 2px var(--shadow-color); 12 | border-radius: 8px; 13 | 14 | max-height: 90vh; 15 | width: min(100%, 500px); 16 | box-sizing: border-box; 17 | 18 | &::backdrop { 19 | backdrop-filter: blur(5px); 20 | -webkit-backdrop-filter: blur(5px); 21 | background-color: var(--background-color-transparent); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/dom/createButton.ts: -------------------------------------------------------------------------------- 1 | import createElement from './createElement'; 2 | 3 | interface Options { 4 | onClick?: (event: MouseEvent) => void; 5 | text?: string; 6 | classList?: string[]; 7 | } 8 | 9 | const createButton = ({ onClick, text, classList = [] }: Options = {}) => { 10 | const button = createElement('button', { type: 'button' }); 11 | 12 | if (onClick) { 13 | button.onclick = onClick; 14 | } 15 | if (text) { 16 | button.textContent = text; 17 | } 18 | for (const className of classList) { 19 | button.classList.add(className); 20 | } 21 | 22 | return button; 23 | }; 24 | 25 | export default createButton; 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | environment: publishing 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v6 14 | - uses: actions/setup-node@v6 15 | with: 16 | node-version: '24.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - run: corepack enable 19 | - run: yarn install 20 | - name: Run tests 21 | run: yarn run test 22 | - name: Publish 23 | run: yarn run publish-ci 24 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/utils/localization.ts: -------------------------------------------------------------------------------- 1 | export interface ToolbarUtilsLocalization { 2 | help: string; 3 | helpScreenNavigationHelp: string; 4 | helpControlsAccessibilityLabel: string; 5 | helpHidden: string; 6 | next: string; 7 | previous: string; 8 | close: string; 9 | } 10 | 11 | export const defaultToolbarUtilsLocalization: ToolbarUtilsLocalization = { 12 | help: 'Help', 13 | helpHidden: 'Help hidden', 14 | next: 'Next', 15 | previous: 'Previous', 16 | close: 'Close', 17 | helpScreenNavigationHelp: 'Click on a control for more information.', 18 | helpControlsAccessibilityLabel: 'Controls: Activate a control to show help.', 19 | }; 20 | -------------------------------------------------------------------------------- /docs/debugging/input-system-tester/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/input-system-tester", 3 | "version": "1.32.0", 4 | "description": "Allows comparing different stroke smoothing implementations", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "bundle": "ts-node scripts/bundle.ts", 9 | "watchBundle": "ts-node scripts/watchBundle.ts", 10 | "build": "build-tool build", 11 | "watch": "build-tool watch" 12 | }, 13 | "dependencies": { 14 | "@js-draw/debugging": "^1.32.0", 15 | "@js-draw/material-icons": "^1.32.0", 16 | "js-draw": "^1.32.0" 17 | }, 18 | "devDependencies": { 19 | "@js-draw/build-tool": "^1.32.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/createEditor.ts: -------------------------------------------------------------------------------- 1 | import { RenderingMode } from '../rendering/Display'; 2 | import Editor, { EditorSettings } from '../Editor'; 3 | import getLocalizationTable from '../localizations/getLocalizationTable'; 4 | 5 | /** Creates an editor. Should only be used in test files. */ 6 | export default (settings?: Partial) => { 7 | if (jest === undefined) { 8 | throw new Error('Files in the testing/ folder should only be used in tests!'); 9 | } 10 | 11 | return new Editor(document.body, { 12 | renderingMode: RenderingMode.DummyRenderer, 13 | localization: getLocalizationTable(['en']), 14 | ...settings, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/InkHighlighter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/js-draw/src/components/util/describeComponentList.ts: -------------------------------------------------------------------------------- 1 | import AbstractComponent from '../AbstractComponent'; 2 | import { ImageComponentLocalization } from '../localization'; 3 | 4 | // Returns the description of all given elements, if identical, otherwise, 5 | // returns null. 6 | export default (localizationTable: ImageComponentLocalization, elems: AbstractComponent[]) => { 7 | if (elems.length === 0) { 8 | return null; 9 | } 10 | 11 | const description = elems[0].description(localizationTable); 12 | for (const elem of elems) { 13 | if (elem.description(localizationTable) !== description) { 14 | return null; 15 | } 16 | } 17 | return description; 18 | }; 19 | -------------------------------------------------------------------------------- /docs/doc-pages/pages/migrations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Migrations 3 | group: Migrations 4 | children: 5 | - ./migrations/migrating-to-v1.md 6 | --- 7 | 8 | # Updating js-draw 9 | 10 | In general, `js-draw` releases follow [Semantic Versioning (semver)](https://github.com/semver/semver/blob/master/semver.md). This means that version numbers are broken up into three parts: 11 | 12 | - The major version (e.g. 2 in **2**.1.0). 13 | - The minor version (e.g. 1 in 2.**1**.0). 14 | - The patch version (e.g. 0 in 2.1.**0**). 15 | 16 | Only major version updates should include breaking changes. When a new major version is released, a new document should be added under this "Migrations" section. 17 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/bytesToSizeString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a size in bytes, KiB, or MiB with units suffix. 3 | */ 4 | const bytesToSizeString = (sizeBytes: number) => { 5 | const sizeInKiB = sizeBytes / 1024; 6 | const sizeInMiB = sizeInKiB / 1024; 7 | const sizeInGiB = sizeInMiB / 1024; 8 | 9 | let units = 'B'; 10 | let size = sizeBytes; 11 | 12 | if (sizeInGiB >= 1) { 13 | size = sizeInGiB; 14 | units = 'GiB'; 15 | } else if (sizeInMiB >= 1) { 16 | size = sizeInMiB; 17 | units = 'MiB'; 18 | } else if (sizeInKiB >= 1) { 19 | size = sizeInKiB; 20 | units = 'KiB'; 21 | } 22 | 23 | return { size, units }; 24 | }; 25 | 26 | export default bytesToSizeString; 27 | -------------------------------------------------------------------------------- /testing/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-object-type */ 2 | 3 | // Type declarations for custom matchers 4 | interface CustomMatchers { 5 | objEq( 6 | expected: { 7 | eq: (other: any, ...args: Args) => boolean; 8 | }, 9 | ...opts: unknown[] 10 | ): R; 11 | 12 | toHaveEntriesCloseTo(expected: number[], tolerance?: number): R; 13 | } 14 | 15 | declare namespace jest { 16 | interface Expect extends CustomMatchers {} 17 | interface Matchers extends CustomMatchers {} 18 | interface AsyncAsymmetricMatchers extends CustomMatchers {} 19 | } 20 | 21 | declare interface JestMatchers extends CustomMatchers {} 22 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/dom/waitForImageLoaded.ts: -------------------------------------------------------------------------------- 1 | const waitForImageLoad = async (image: HTMLImageElement) => { 2 | if (!image.complete) { 3 | await new Promise((resolve, reject) => { 4 | image.onload = (event) => resolve(event); 5 | 6 | // TODO(v2): Return a `new Error(event.message)` 7 | // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- Forwarding an error-like object. 8 | image.onerror = (event) => reject(event); 9 | // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- Forwarding an error-like object. 10 | image.onabort = (event) => reject(event); 11 | }); 12 | } 13 | }; 14 | 15 | export default waitForImageLoad; 16 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/waitForAll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Resolves when all given promises have resolved. If no promises are given, 3 | * does not return a Promise. 4 | * 5 | * If all elements of `results` are known to be `Promise`s, use `Promise.all`. 6 | */ 7 | const waitForAll = (results: (Promise | void)[]): Promise | void => { 8 | // If any are Promises... 9 | if (results.some((command) => command && command['then'])) { 10 | // Wait for all commands to finish. 11 | return ( 12 | Promise.all(results) 13 | // Ensure we return a Promise and not a Promise 14 | .then(() => {}) 15 | ); 16 | } 17 | 18 | return; 19 | }; 20 | 21 | export default waitForAll; 22 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Draw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/sendKeyPressRelease.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../Editor'; 2 | import { InputEvtType } from '../inputEvents'; 3 | import guessKeyCodeFromKey from '../util/guessKeyCodeFromKey'; 4 | 5 | const sendKeyPressRelease = (target: Editor | HTMLElement, key: string) => { 6 | if (target instanceof Editor) { 7 | target.sendKeyboardEvent(InputEvtType.KeyPressEvent, key); 8 | target.sendKeyboardEvent(InputEvtType.KeyUpEvent, key); 9 | } else { 10 | const code = guessKeyCodeFromKey(key); 11 | target.dispatchEvent(new KeyboardEvent('keydown', { key, code })); 12 | target.dispatchEvent(new KeyboardEvent('keyup', { key, code })); 13 | } 14 | }; 15 | 16 | export default sendKeyPressRelease; 17 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/DropdownToolbar.test.ts: -------------------------------------------------------------------------------- 1 | import createEditor from '../testing/createEditor'; 2 | import findNodeWithText from '../testing/findNodeWithText'; 3 | import { makeDropdownToolbar } from './DropdownToolbar'; 4 | 5 | describe('DropdownToolbar', () => { 6 | test('should default to using localizations from the editor', () => { 7 | const testLocalizationOverride = 'testing-testing-testing'; 8 | const editor = createEditor({ 9 | localization: { 10 | pen: testLocalizationOverride, 11 | }, 12 | }); 13 | const toolbar = makeDropdownToolbar(editor); 14 | toolbar.addDefaults(); 15 | expect(findNodeWithText(testLocalizationOverride, editor.getRootElement())); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/PenToolWidget.scss: -------------------------------------------------------------------------------- 1 | // Repeat selector for extra specificity 2 | :root .toolbar--pen-tool-toggle-buttons.toolbar--pen-tool-toggle-buttons { 3 | display: flex; 4 | flex-wrap: wrap; 5 | justify-content: stretch; 6 | padding-top: 0; 7 | padding-bottom: 5px; 8 | gap: 5px; 9 | 10 | // Some styles rely on left being start. 11 | direction: ltr; 12 | 13 | & > * { 14 | flex-grow: 1; 15 | text-align: start; 16 | width: min-content; 17 | 18 | > .icon { 19 | margin-inline-start: 6px; 20 | margin-inline-end: 10px; 21 | } 22 | } 23 | 24 | & > :nth-child(2) { 25 | text-align: center; 26 | } 27 | 28 | & > :nth-child(3) { 29 | direction: rtl; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/InsertImageWidget/fileToImages.ts: -------------------------------------------------------------------------------- 1 | import type { RenderableImage } from '../../../rendering/renderers/AbstractRenderer'; 2 | import fileToBase64Url from '../../../util/fileToBase64Url'; 3 | import { Mat33 } from '@js-draw/math'; 4 | 5 | const fileToImages = async (imageFile: File): Promise => { 6 | const result: RenderableImage[] = []; 7 | 8 | const imageElement = new Image(); 9 | 10 | const base64Url = await fileToBase64Url(imageFile); 11 | if (base64Url) { 12 | result.push({ 13 | image: imageElement, 14 | base64Url: base64Url, 15 | transform: Mat33.identity, 16 | }); 17 | } 18 | 19 | return result; 20 | }; 21 | 22 | export default fileToImages; 23 | -------------------------------------------------------------------------------- /scripts/build-website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Builds and runs for GitHub pages/firebase 4 | # Should only be run by continuous integration and called 5 | # AFTER installing dependencies and running tests. 6 | 7 | # Ref: https://stackoverflow.com/a/1482133 8 | script_dir=$(dirname -- $(realpath "$0")) 9 | root_dir=$(dirname -- "$script_dir") 10 | 11 | cd "$root_dir" 12 | 13 | # Build all TypeDoc documentation 14 | echo 'Building documentation' 15 | yarn run doc 16 | 17 | # Build the main example app 18 | echo 'Build demo app' 19 | cd docs/demo 20 | yarn run build 21 | 22 | # Create symlinks between files/directories that have moved 23 | echo 'Symlink old paths' 24 | cd "$root_dir" 25 | cd docs/ 26 | ln -s demo example 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/Imagesmode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/js-draw/src/rendering/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as AbstractRenderer } from './renderers/AbstractRenderer'; 2 | export { default as DummyRenderer } from './renderers/DummyRenderer'; 3 | export { default as SVGRenderer } from './renderers/SVGRenderer'; 4 | export { default as CanvasRenderer } from './renderers/CanvasRenderer'; 5 | export { default as Display, RenderingMode } from './Display'; 6 | export { default as TextRenderingStyle } from './TextRenderingStyle'; 7 | export { default as RenderingStyle, StrokeStyle as StrokeRenerdingStyle } from './RenderingStyle'; 8 | export { 9 | pathToRenderable, 10 | pathFromRenderable, 11 | visualEquivalent as pathVisualEquivalent, 12 | default as RenderablePathSpec, 13 | } from './RenderablePathSpec'; 14 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-on-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests on PR 2 | on: 3 | pull_request: 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build_and_test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | - name: Prepare yarn 14 | run: corepack enable 15 | - name: Install dependencies 16 | run: yarn install 17 | - name: Test imports 18 | run: yarn run dist-test 19 | - name: Run tests 20 | run: yarn run test 21 | - name: Lint 22 | run: yarn run lint-ci 23 | - name: Check formatting 24 | run: bash scripts/check-formatting-ci.sh 25 | - name: Build example app 26 | run: cd docs/demo && yarn run build 27 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/fillHtmlInput.ts: -------------------------------------------------------------------------------- 1 | import sendKeyPressRelease from './sendKeyPressRelease'; 2 | 3 | interface Options { 4 | clear?: boolean; 5 | } 6 | 7 | /** Sets the content of the given `input` or textarea to be `text`. */ 8 | const fillInput = ( 9 | input: HTMLInputElement | HTMLTextAreaElement, 10 | text: string, 11 | { clear = false }: Options = {}, 12 | ) => { 13 | const dispatchUpdate = () => { 14 | input.dispatchEvent(new InputEvent('input')); 15 | }; 16 | if (clear) { 17 | input.value = ''; 18 | dispatchUpdate(); 19 | } 20 | 21 | for (const character of text.split('')) { 22 | input.value += character; 23 | sendKeyPressRelease(input, character); 24 | dispatchUpdate(); 25 | } 26 | }; 27 | 28 | export default fillInput; 29 | -------------------------------------------------------------------------------- /packages/js-draw/dist-test/test_imports/test-require.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | 3 | console.log('Testing require()...'); 4 | 5 | // TODO: Test require('js-draw') (requires removing Coloris because 6 | // Coloris depends on the DOM when first loaded). 7 | 8 | const { TextComponent, StrokeComponent } = require('js-draw/components'); 9 | require('./dom-mocks.cjs'); 10 | const { Editor } = require('js-draw/Editor'); 11 | 12 | if (!Editor) { 13 | throw new Error('Failed to import Editor'); 14 | } 15 | 16 | if (!TextComponent.fromLines) { 17 | throw new Error('Failed to import module TextComponent'); 18 | } 19 | 20 | if (!StrokeComponent.deserializeFromJSON) { 21 | throw new Error('Failed to import StrokeComponent'); 22 | } 23 | -------------------------------------------------------------------------------- /scripts/check-formatting-ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Checks formatting in CI and, if different, prints a diff. 3 | 4 | if ! yarn check-formatting-ci ; then 5 | echo "Stashing changes..." 6 | 7 | # Ensure at least one file is present to stash 8 | touch checking-formatting.status 9 | git stash push --include-untracked --quiet 10 | 11 | echo "Running the formatter..." 12 | yarn format . >/dev/null 13 | 14 | echo "Difference after running the formatter:" 15 | echo "" 16 | git diff HEAD 17 | echo "" 18 | 19 | echo "Restoring changes..." 20 | git restore . 21 | git stash pop --quiet 22 | rm checking-formatting.status 23 | 24 | echo "[❗] Formatting errors detected. It should be possible to fix them by running 'yarn format .' [❗]" 25 | exit 1 26 | fi -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as ActionButtonWidget } from './ActionButtonWidget'; 2 | export { default as BaseToolWidget } from './BaseToolWidget'; 3 | export { default as BaseWidget, ToolbarWidgetTag } from './BaseWidget'; 4 | 5 | export { default as PenToolWidget } from './PenToolWidget'; 6 | export { default as TextToolWidget } from './TextToolWidget'; 7 | export { default as HandToolWidget } from './HandToolWidget'; 8 | export { default as SelectionToolWidget } from './SelectionToolWidget'; 9 | export { default as EraserToolWidget } from './EraserToolWidget'; 10 | 11 | export { default as InsertImageWidget } from './InsertImageWidget/InsertImageWidget'; 12 | export { default as DocumentPropertiesWidget } from './DocumentPropertiesWidget'; 13 | -------------------------------------------------------------------------------- /docs/demo/sw.js: -------------------------------------------------------------------------------- 1 | // Service worker. 2 | // See https://developer.chrome.com/docs/workbox/caching-strategies-overview/ for service-worker-related documentation. 3 | 4 | const cacheName = 'v1'; 5 | 6 | self.addEventListener('fetch', (event) => { 7 | // We need to pass a Promise to event.respondWith — event.respondWith 8 | // must be called synchronously within 'fetch'. See 9 | // https://stackoverflow.com/a/46839810 10 | event.respondWith( 11 | (async () => { 12 | const cache = await caches.open(cacheName); 13 | try { 14 | const fetched = await fetch(event.request.url); 15 | cache.put(event.request, fetched.clone()); 16 | return fetched; 17 | } catch (e) { 18 | console.warn('Error fetching,', e); 19 | return cache.match(event.request.url); 20 | } 21 | })(), 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /docs/doc-pages/pages/guides.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | category: Guides 4 | children: 5 | - ./guides/setup.md 6 | - ./guides/writing-a-theme.md 7 | - ./guides/components.md 8 | - ./guides/updating-the-view.md 9 | - ./guides/customizing-tools.md 10 | - ./guides/positioning-an-element-above-the-editor.md 11 | --- 12 | 13 | # Tutorials 14 | 15 | This folder contains additional documention for `js-draw`. 16 | 17 | Before reading these guides, it may be helpful to review the examples in `js-draw`'s [`README`](../#api). 18 | 19 | Additional examples can be found in the [examples/](https://github.com/personalizedrefrigerator/js-draw/tree/main/docs/examples) directory on GitHub. A directory of all inline runnable examples in the documentation can be found at [assets/doctest.html](../assets/doctest.html). 20 | -------------------------------------------------------------------------------- /dist-test/icons-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Editor from a bundle 7 | 8 | 9 |

10 | This file tests the bundled version of js-draw. Be sure to run 11 | yarn run build before opening this! 12 |

13 | 14 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/build-tool/src/utils/forEachFileInDirectory.ts: -------------------------------------------------------------------------------- 1 | import { readdir, stat } from 'fs/promises'; 2 | import path from 'path'; 3 | 4 | // Iterates over every file in [directory]. 5 | const forEachFileInDirectory = async ( 6 | directory: string, 7 | processFile: (filePath: string) => Promise, 8 | ) => { 9 | const files = await readdir(directory); 10 | 11 | await Promise.all( 12 | files.map(async (file) => { 13 | const filePath = path.join(directory, file); 14 | const stats = await stat(filePath); 15 | 16 | if (stats.isDirectory()) { 17 | await forEachFileInDirectory(filePath, processFile); 18 | } else if (stats.isFile()) { 19 | await processFile(filePath); 20 | } else { 21 | throw new Error('Unknown file type!'); 22 | } 23 | }), 24 | ); 25 | }; 26 | 27 | export default forEachFileInDirectory; 28 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/tools/prebuild.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | const fs = require('node:fs'); 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-require-imports 5 | const path = require('node:path'); 6 | 7 | console.log('Copying licenses...'); 8 | const rootDir = path.dirname(__dirname); 9 | const distDir = path.join(rootDir, 'dist'); 10 | fs.copyFileSync(path.join(rootDir, 'dependency-licenses.txt'), path.join(distDir, 'licenses.txt')); 11 | 12 | console.log('Writing version...'); 13 | // eslint-disable-next-line @typescript-eslint/no-require-imports 14 | const version = require('../package.json').version; 15 | fs.writeFileSync( 16 | path.join(rootDir, 'src', 'constants.autogenerated.ts'), 17 | ` 18 | export default { 19 | version: ${JSON.stringify(version)}, 20 | }; 21 | `, 22 | ); 23 | -------------------------------------------------------------------------------- /docs/demo/storage/makeReadOnlyStoreEntry.ts: -------------------------------------------------------------------------------- 1 | import { makeIconFromText } from '../icons'; 2 | import { StoreEntry } from './AbstractStore'; 3 | 4 | /** Returns a `StoreEntry` that returns the given content, but cannot be updated/saved. */ 5 | const makeReadOnlyStoreEntry = (content: string, onIllegalOperation?: () => void): StoreEntry => { 6 | return { 7 | title: 'Read-only store entry', 8 | getIcon() { 9 | return makeIconFromText('!'); 10 | }, 11 | delete: async () => { 12 | console.warn('Attempt to delete a dummyStoreEntry'); 13 | onIllegalOperation?.(); 14 | }, 15 | read: async () => content, 16 | write: async () => { 17 | console.warn('Attempt to write to a dummyStoreEntry'); 18 | onIllegalOperation?.(); 19 | }, 20 | updatePreview: null, 21 | updateTitle: null, 22 | }; 23 | }; 24 | export default makeReadOnlyStoreEntry; 25 | -------------------------------------------------------------------------------- /packages/math/src/rounding/cleanUpNumber.test.ts: -------------------------------------------------------------------------------- 1 | import cleanUpNumber from './cleanUpNumber'; 2 | 3 | it('cleanUpNumber', () => { 4 | expect(cleanUpNumber('000.0000')).toBe('0'); 5 | expect(cleanUpNumber('-000.0000')).toBe('0'); 6 | expect(cleanUpNumber('0.0000')).toBe('0'); 7 | expect(cleanUpNumber('0.001')).toBe('.001'); 8 | expect(cleanUpNumber('-0.001')).toBe('-.001'); 9 | expect(cleanUpNumber('-0.000000001')).toBe('-.000000001'); 10 | expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001'); 11 | expect(cleanUpNumber('1234')).toBe('1234'); 12 | expect(cleanUpNumber('1234.5')).toBe('1234.5'); 13 | expect(cleanUpNumber('1234.500')).toBe('1234.5'); 14 | expect(cleanUpNumber('1234.00500')).toBe('1234.005'); 15 | expect(cleanUpNumber('1234.001234500')).toBe('1234.0012345'); 16 | expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0'); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/math/src/rounding/getLenAfterDecimal.ts: -------------------------------------------------------------------------------- 1 | import { numberRegex } from './constants'; 2 | 3 | /** 4 | * Returns the length of `numberAsString` after a decimal point. 5 | * 6 | * For example, 7 | * ```ts 8 | * getLenAfterDecimal('1.001') // -> 3 9 | * ``` 10 | */ 11 | export const getLenAfterDecimal = (numberAsString: string) => { 12 | const numberMatch = numberRegex.exec(numberAsString); 13 | if (!numberMatch) { 14 | // If not a match, either the number is exponential notation (or is something 15 | // like NaN or Infinity) 16 | if (numberAsString.search(/[eE]/) !== -1 || /^[a-zA-Z]+$/.exec(numberAsString)) { 17 | return -1; 18 | // Or it has no decimal point 19 | } else { 20 | return 0; 21 | } 22 | } 23 | 24 | const afterDecimalLen = numberMatch[3].length; 25 | return afterDecimalLen; 26 | }; 27 | 28 | export default getLenAfterDecimal; 29 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/src/browser/editor/loadIframePreviewScript.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { assetsPath } from '../constants'; 3 | 4 | let iframePreviewScript: string | null = null; // null if not loaded 5 | 6 | /** 7 | * Loads the script used to set up the editor's iframe. 8 | * 9 | * @internal 10 | */ 11 | const loadIframePreviewScript = async () => { 12 | if (iframePreviewScript) { 13 | return iframePreviewScript; 14 | } 15 | 16 | const scriptPath = join(assetsPath, 'js-draw-typedoc-extension--iframe.js'); 17 | 18 | const scriptRequest = await fetch(scriptPath); 19 | const scriptContent = await scriptRequest.text(); 20 | 21 | // Allow including inline in the iframe. 22 | iframePreviewScript = scriptContent.replace(/<[/]script>/g, '<\\/script>'); 23 | return iframePreviewScript; 24 | }; 25 | 26 | export default loadIframePreviewScript; 27 | -------------------------------------------------------------------------------- /packages/js-draw/src/dialogs/makeMessageDialog.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade-in { 2 | from { 3 | opacity: 0; 4 | } 5 | to { 6 | opacity: 1; 7 | } 8 | } 9 | 10 | .message-dialog-container dialog { 11 | display: flex; 12 | 13 | &.-closing { 14 | opacity: 0; 15 | 16 | &::backdrop { 17 | opacity: 0; 18 | } 19 | } 20 | 21 | &, 22 | &::backdrop { 23 | transition: opacity 0.2s ease; 24 | animation: fade-in 0.2s ease; 25 | } 26 | } 27 | 28 | .message-dialog-content { 29 | display: flex; 30 | flex-direction: column; 31 | flex-grow: 1; 32 | 33 | > .close { 34 | // Center the close button 35 | display: block; 36 | margin-left: auto; 37 | margin-right: auto; 38 | } 39 | 40 | > .scroll { 41 | flex-grow: 1; 42 | flex-shrink: 1; 43 | overflow-y: auto; 44 | 45 | margin-left: 20px; 46 | margin-right: 20px; 47 | 48 | padding-bottom: 20px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/js-draw/src/components/builders/FreehandLineBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import Viewport from '../../Viewport'; 2 | import { Vec2, Color4 } from '@js-draw/math'; 3 | import { StrokeDataPoint } from '../../types'; 4 | import { makeFreehandLineBuilder } from './FreehandLineBuilder'; 5 | 6 | describe('FreehandLineBuilder', () => { 7 | it('should create a dot on click', () => { 8 | const viewport = new Viewport(() => {}); 9 | const startPoint: StrokeDataPoint = { 10 | pos: Vec2.zero, 11 | width: 1, 12 | time: performance.now(), 13 | color: Color4.red, 14 | }; 15 | const lineBuilder = makeFreehandLineBuilder(startPoint, viewport); 16 | 17 | const component = lineBuilder.build(); 18 | const bbox = component.getBBox(); 19 | 20 | expect(bbox.area).toBeGreaterThan(0); 21 | expect(bbox.width).toBeGreaterThan(0.5); 22 | expect(bbox.height).toBeGreaterThan(0.5); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /docs/demo/ui/newImageDialog.css: -------------------------------------------------------------------------------- 1 | .new-image-template-button { 2 | background-color: #eee; 3 | border: none; 4 | color: black; 5 | border-radius: 5px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | } 11 | 12 | .new-image-template-button div { 13 | text-align: center; 14 | } 15 | 16 | .new-image-template-button .icon { 17 | width: 300px; 18 | height: 150px; 19 | 20 | object-position: left top; 21 | object-fit: none; /* Do not resize to fit container */ 22 | } 23 | 24 | .new-image-template-button:hover { 25 | background-color: #ddd; 26 | } 27 | 28 | .from-template-container { 29 | max-height: 400px; 30 | overflow-y: auto; 31 | } 32 | 33 | @media (prefers-color-scheme: dark) { 34 | .new-image-template-button { 35 | background-color: #222; 36 | color: white; 37 | } 38 | 39 | .new-image-template-button:hover { 40 | background-color: #444; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/OverflowWidget.css: -------------------------------------------------------------------------------- 1 | .toolbar-overflow-widget-overflow-list { 2 | display: flex; 3 | flex-direction: column; 4 | flex-wrap: wrap; 5 | justify-content: center; 6 | } 7 | 8 | .toolbar-overflow-widget-overflow-list > .toolbar-toolContainer > .toolbar-button { 9 | height: var(--toolbar-button-height); 10 | } 11 | 12 | .toolbar-overflow-widget.horizontal .toolbar-overflow-widget-overflow-list { 13 | flex-direction: row; 14 | } 15 | 16 | .toolbar-overflow-widget.horizontal > .toolbar-dropdown { 17 | max-width: 100%; 18 | left: 15px; 19 | right: 15px; 20 | 21 | /* 22 | Override the default transform and margin-left. 23 | 24 | Setting translate to none prevents the dropdown from being shifted off the 25 | screen on window resize by dropdown-repositioning logic. 26 | */ 27 | margin-left: 0 !important; 28 | translate: none !important; 29 | 30 | padding: 4px; 31 | } 32 | -------------------------------------------------------------------------------- /packages/js-draw/src/rendering/localization.ts: -------------------------------------------------------------------------------- 1 | export interface TextRendererLocalization { 2 | pathNodeCount(pathCount: number): string; 3 | textNodeCount(nodeCount: number): string; 4 | imageNodeCount(nodeCount: number): string; 5 | textNode(content: string): string; 6 | unlabeledImageNode: string; 7 | imageNode(label: string): string; 8 | rerenderAsText: string; 9 | } 10 | 11 | export const defaultTextRendererLocalization: TextRendererLocalization = { 12 | pathNodeCount: (count: number) => `There are ${count} visible path objects.`, 13 | textNodeCount: (count: number) => `There are ${count} visible text nodes.`, 14 | imageNodeCount: (nodeCount: number) => `There are ${nodeCount} visible image nodes.`, 15 | textNode: (content: string) => `Text: ${content}`, 16 | imageNode: (label: string) => `Image: ${label}`, 17 | unlabeledImageNode: 'Unlabeled image', 18 | rerenderAsText: 'Re-render as text', 19 | }; 20 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/findNodeWithText.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | tag?: string; 3 | } 4 | 5 | /** Returns the first node or element with `textContent` matching `expectedText`. */ 6 | const findNodeWithText = ( 7 | expectedText: string, 8 | parent: Node, 9 | options: Options = {}, 10 | ): Node | null => { 11 | const { tag } = options; 12 | 13 | if (parent.textContent === expectedText) { 14 | const matchesTag = (() => { 15 | // No tag check necessary? 16 | if (!tag) return true; 17 | return parent instanceof Element && tag.toUpperCase() === parent.tagName; 18 | })(); 19 | 20 | if (matchesTag) { 21 | return parent; 22 | } 23 | } 24 | 25 | for (const child of parent.childNodes) { 26 | const results = findNodeWithText(expectedText, child, options); 27 | if (results) { 28 | return results; 29 | } 30 | } 31 | 32 | return null; 33 | }; 34 | 35 | export default findNodeWithText; 36 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/UndoRedoShortcut.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../Editor'; 2 | import { KeyPressEvent } from '../inputEvents'; 3 | import BaseTool from './BaseTool'; 4 | import { redoKeyboardShortcutId, undoKeyboardShortcutId } from './keybindings'; 5 | 6 | // Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts. 7 | export default class UndoRedoShortcut extends BaseTool { 8 | public constructor(private editor: Editor) { 9 | super(editor.notifier, editor.localization.undoRedoTool); 10 | } 11 | 12 | // @internal 13 | public override onKeyPress(event: KeyPressEvent): boolean { 14 | if (this.editor.shortcuts.matchesShortcut(undoKeyboardShortcutId, event)) { 15 | void this.editor.history.undo(); 16 | return true; 17 | } else if (this.editor.shortcuts.matchesShortcut(redoKeyboardShortcutId, event)) { 18 | void this.editor.history.redo(); 19 | return true; 20 | } 21 | 22 | return false; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/guessKeyCodeFromKey.ts: -------------------------------------------------------------------------------- 1 | // See https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values for 2 | // more 3 | const keyToKeyCode: Record = { 4 | Control: 'ControlLeft', 5 | '=': 'Equal', 6 | '-': 'Minus', 7 | ';': 'Semicolon', 8 | ' ': 'Space', 9 | }; 10 | 11 | /** 12 | * Attempts to guess the .code value corresponding to the given key. 13 | * 14 | * Use this to facilitate testing. 15 | * 16 | * If no matching keycode is found, returns `key`. 17 | */ 18 | const guessKeyCodeFromKey = (key: string) => { 19 | const upperKey = key.toUpperCase(); 20 | if ('A' <= upperKey && upperKey <= 'Z') { 21 | return `Key${upperKey}`; 22 | } 23 | 24 | if ('0' <= key && key <= '9') { 25 | return `Digit${key}`; 26 | } 27 | 28 | if (key in keyToKeyCode) { 29 | return keyToKeyCode[key]; 30 | } 31 | 32 | return key; 33 | }; 34 | 35 | export default guessKeyCodeFromKey; 36 | -------------------------------------------------------------------------------- /docs/demo/storage/makeFileSaver.ts: -------------------------------------------------------------------------------- 1 | import ImageSaver from './ImageSaver'; 2 | 3 | /** 4 | * Returns an `ImageSaver` that can update the content of a `FileSystemHandle`. 5 | * 6 | * This is used by {@link showSavePopup}. 7 | */ 8 | const makeFileSaver = (fileName: string, file: FileSystemHandle): ImageSaver => { 9 | return { 10 | title: fileName, 11 | write: async (svgData: string): Promise => { 12 | try { 13 | // As of 2/21/2023, TypeScript does not recognise createWritable 14 | // as a property of FileSystemHandle. 15 | const writable = await (file as any).createWritable(); 16 | await writable.write(svgData); 17 | await writable.close(); 18 | } catch (e) { 19 | throw new Error(`Error saving to filesystem: ${e}`); 20 | } 21 | }, 22 | 23 | // Doesn't support updating the title/preview. 24 | updateTitle: null, 25 | updatePreview: null, 26 | }; 27 | }; 28 | 29 | export default makeFileSaver; 30 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/fileToBase64Url.test.ts: -------------------------------------------------------------------------------- 1 | import fileToBase64Url from './fileToBase64Url'; 2 | 3 | // Use NodeJS's Blob (jsdom's Blob doesn't support .arrayBuffer). 4 | // eslint-disable-next-line @typescript-eslint/no-require-imports 5 | const { Blob } = require('node:buffer'); 6 | 7 | const originalFileReader = window.FileReader; 8 | 9 | describe('fileToBase64Url', () => { 10 | afterEach(() => { 11 | window.FileReader = originalFileReader; 12 | }); 13 | 14 | it("should convert a Blob to a base64 URL if FileReader can't load", async () => { 15 | window.FileReader = undefined as any; 16 | 17 | const onWarning = jest.fn(); 18 | 19 | const blob = new Blob([new Uint8Array([1, 2, 3, 4]).buffer], { type: 'text/plain' }); 20 | expect(await fileToBase64Url(blob, { onWarning })).toBe('data:text/plain;base64,AQIDBA=='); 21 | 22 | // Should have triggered a warning 23 | expect(onWarning).toHaveBeenCalled(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/PanTool.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/math/src/shapes/CubicBezier.ts: -------------------------------------------------------------------------------- 1 | import { Point2 } from '../Vec2'; 2 | import BezierJSWrapper from './BezierJSWrapper'; 3 | import Rect2 from './Rect2'; 4 | 5 | /** 6 | * A wrapper around [`bezier-js`](https://github.com/Pomax/bezierjs)'s cubic Bezier. 7 | */ 8 | class CubicBezier extends BezierJSWrapper { 9 | public constructor( 10 | // Start point 11 | public readonly p0: Point2, 12 | 13 | // Control point 1 14 | public readonly p1: Point2, 15 | 16 | // Control point 2 17 | public readonly p2: Point2, 18 | 19 | // End point 20 | public readonly p3: Point2, 21 | ) { 22 | super(); 23 | } 24 | 25 | public override getPoints() { 26 | return [this.p0, this.p1, this.p2, this.p3]; 27 | } 28 | 29 | /** Returns an overestimate of this shape's bounding box. */ 30 | public override getLooseBoundingBox(): Rect2 { 31 | return Rect2.bboxOf([this.p0, this.p1, this.p2, this.p3]); 32 | } 33 | } 34 | 35 | export default CubicBezier; 36 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/sendHtmlSwipe.ts: -------------------------------------------------------------------------------- 1 | import { Point2 } from '@js-draw/math'; 2 | 3 | /** Swipes `element` using HTML pointer events. */ 4 | const sendHtmlSwipe = async ( 5 | element: HTMLElement, 6 | start: Point2, 7 | end: Point2, 8 | timeMs: number = 300, 9 | ) => { 10 | element.dispatchEvent( 11 | new PointerEvent('pointerdown', { isPrimary: true, clientX: start.x, clientY: start.y }), 12 | ); 13 | 14 | const step = 0.1; 15 | for (let i = 0; i < 1; i += step) { 16 | await jest.advanceTimersByTimeAsync(timeMs * step); 17 | 18 | const currentPoint = start.lerp(end, i); 19 | element.dispatchEvent( 20 | new PointerEvent('pointermove', { 21 | isPrimary: true, 22 | clientX: currentPoint.x, 23 | clientY: currentPoint.y, 24 | }), 25 | ); 26 | } 27 | 28 | element.dispatchEvent( 29 | new PointerEvent('pointerup', { isPrimary: true, clientX: end.x, clientY: end.y }), 30 | ); 31 | }; 32 | 33 | export default sendHtmlSwipe; 34 | -------------------------------------------------------------------------------- /packages/js-draw/tools/prebuild.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | const path = require('node:path'); 3 | // eslint-disable-next-line @typescript-eslint/no-require-imports 4 | const fs = require('node:fs'); 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-require-imports 7 | const version = require('../package.json').version; 8 | 9 | const updateVersion = () => { 10 | const versionPath = path.join(path.dirname(__dirname), 'src', 'version.ts'); 11 | const versionContent = fs.readFileSync(versionPath, 'utf8'); 12 | 13 | let didReplace = false; 14 | const updatedContent = versionContent.replace(/number: '.*'/, () => { 15 | didReplace = true; 16 | return `number: '${version}'`; 17 | }); 18 | 19 | if (!didReplace) { 20 | throw new Error('Version number auto-updater: Unable to find a version number to update.'); 21 | } 22 | 23 | fs.writeFileSync(versionPath, updatedContent); 24 | }; 25 | 26 | updateVersion(); 27 | -------------------------------------------------------------------------------- /packages/js-draw/src/image/export/adjustExportedSVGSize.ts: -------------------------------------------------------------------------------- 1 | import { Rect2, toRoundedString } from '@js-draw/math'; 2 | 3 | // @internal 4 | export type SVGSizingOptions = { minDimension?: number }; 5 | 6 | // @internal 7 | const adjustExportedSVGSize = (svg: SVGElement, exportRect: Rect2, options: SVGSizingOptions) => { 8 | // Adjust the width/height as necessary 9 | let width = exportRect.w; 10 | let height = exportRect.h; 11 | 12 | if (options?.minDimension && width < options.minDimension) { 13 | const newWidth = options.minDimension; 14 | height *= newWidth / (width || 1); 15 | width = newWidth; 16 | } 17 | 18 | if (options?.minDimension && height < options.minDimension) { 19 | const newHeight = options.minDimension; 20 | width *= newHeight / (height || 1); 21 | height = newHeight; 22 | } 23 | 24 | svg.setAttribute('width', toRoundedString(width)); 25 | svg.setAttribute('height', toRoundedString(height)); 26 | }; 27 | 28 | export default adjustExportedSVGSize; 29 | -------------------------------------------------------------------------------- /packages/js-draw/src/components/localization.ts: -------------------------------------------------------------------------------- 1 | export interface ImageComponentLocalization { 2 | unlabeledImageNode: string; 3 | text: (text: string) => string; 4 | imageNode: (description: string) => string; 5 | stroke: string; 6 | svgObject: string; 7 | emptyBackground: string; 8 | gridBackground: string; 9 | filledBackgroundWithColor: (color: string) => string; 10 | 11 | restyledElement: (elementDescription: string) => string; 12 | } 13 | 14 | export const defaultComponentLocalization: ImageComponentLocalization = { 15 | unlabeledImageNode: 'Unlabeled image node', 16 | stroke: 'Stroke', 17 | svgObject: 'SVG Object', 18 | emptyBackground: 'Empty background', 19 | gridBackground: 'Grid background', 20 | filledBackgroundWithColor: (color) => `Filled background (${color})`, 21 | text: (text) => `Text object: ${text}`, 22 | imageNode: (description: string) => `Image: ${description}`, 23 | restyledElement: (elementDescription: string) => `Restyled ${elementDescription}`, 24 | }; 25 | -------------------------------------------------------------------------------- /docs/doc-pages/inline-examples/canvas-renderer.md: -------------------------------------------------------------------------------- 1 | ```ts,runnable 2 | import {Editor,CanvasRenderer} from 'js-draw'; 3 | 4 | // Create an editor and load initial data -- don't add to the body (hidden editor). 5 | const editor = new Editor(document.createElement('div')); 6 | await editor.loadFromSVG(''); 7 | ---visible--- 8 | // Given some editor. 9 | // Set up the canvas to be drawn onto. 10 | const canvas = document.createElement('canvas'); 11 | const ctx = canvas.getContext('2d'); 12 | 13 | // Ensure that the canvas can fit the entire rendering 14 | const viewport = editor.image.getImportExportViewport(); 15 | canvas.width = viewport.getScreenRectSize().x; 16 | canvas.height = viewport.getScreenRectSize().y; 17 | 18 | // Render editor.image onto the renderer 19 | const renderer = new CanvasRenderer(ctx, viewport); 20 | editor.image.render(renderer, viewport); 21 | 22 | // Add the rendered canvas to the document. 23 | document.body.appendChild(canvas); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples of usage 2 | 3 | - [An index of the examples embedded within the js-draw documentation](https://js-draw.web.app/typedoc/assets/doctest.html) 4 | - [Example of saving and restoring a user's toolbar settings](./examples/example-save-restore-toolbar-state/README.md) 5 | - [Example collaborative editor](./examples/example-collaborative/README.md) 6 | - [Example custom tool and toolbar button](./examples/example-custom-tools/README.md) 7 | - [Source code for the js-draw demo page](./demo/README.md) 8 | 9 | # Sample images created with `js-draw` 10 | 11 | - [![Drawing: js-draw logo](./img/readme-images/logo.svg)](./img/readme-images/logo.svg) 12 | - [![Drawing: Hot air balloon with js-draw written to the left.](./img/sample/sample-1.svg)](./img/sample/sample-1.svg) 13 | - [![Drawing: embedded image labels an arrow pointing to an autosterogram.](./img/sample/sample-2.svg)](./img/sample/sample-2.svg) 14 | - [![Drawing: js-draw written, surrounded by shapes.](./img/sample/sample-3.svg)](./img/sample/sample-3.svg) 15 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/InputFilter/InputPipeline.ts: -------------------------------------------------------------------------------- 1 | import { InputEvt } from '../../inputEvents'; 2 | import InputMapper from './InputMapper'; 3 | 4 | /** 5 | * The composition of multiple `InputMapper`s. 6 | */ 7 | export default class InputPipeline extends InputMapper { 8 | #head: InputMapper | null = null; 9 | #tail: InputMapper | null = null; 10 | 11 | public override onEvent(event: InputEvt): boolean { 12 | if (this.#head === null) { 13 | return this.emit(event); 14 | } else { 15 | return this.#head.onEvent(event); 16 | } 17 | } 18 | 19 | /** 20 | * Adds a new `InputMapper` to the *tail* of this pipeline. 21 | * Note that an instance of an `InputMapper` can only be used in a single 22 | * pipeline. 23 | */ 24 | public addToTail(mapper: InputMapper) { 25 | if (!this.#tail) { 26 | this.#head = mapper; 27 | this.#tail = this.#head; 28 | } else { 29 | this.#tail.setEmitListener(mapper); 30 | this.#tail = mapper; 31 | } 32 | this.#tail.setEmitListener((event) => this.emit(event)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/TouchApp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/js-draw/src/components/AbstractComponent.transformBy.test.ts: -------------------------------------------------------------------------------- 1 | import { Color4, EditorImage, Mat33, Path, Rect2, Vec2 } from '../lib'; 2 | import { pathToRenderable } from '../rendering/RenderablePathSpec'; 3 | import createEditor from '../testing/createEditor'; 4 | import Stroke from './Stroke'; 5 | 6 | describe('AbstractComponent.transformBy', () => { 7 | it("should restore the component's z-index on undo", () => { 8 | const editor = createEditor(); 9 | const component = new Stroke([ 10 | pathToRenderable(Path.fromRect(Rect2.unitSquare), { fill: Color4.red }), 11 | ]); 12 | EditorImage.addComponent(component).apply(editor); 13 | 14 | const origZIndex = component.getZIndex(); 15 | 16 | const transformCommand = component.transformBy(Mat33.translation(Vec2.unitX)); 17 | transformCommand.apply(editor); 18 | 19 | // Should increase the z-index on applying a transform 20 | expect(component.getZIndex()).toBeGreaterThan(origZIndex); 21 | 22 | transformCommand.unapply(editor); 23 | expect(component.getZIndex()).toBe(origZIndex); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/math/src/Vec2.test.ts: -------------------------------------------------------------------------------- 1 | import { Vec2 } from './Vec2'; 2 | import Vec3 from './Vec3'; 3 | 4 | describe('Vec2', () => { 5 | it('Magnitude', () => { 6 | expect(Vec2.of(3, 4).magnitude()).toBe(5); 7 | }); 8 | 9 | it('Addition', () => { 10 | expect(Vec2.of(1, 2).plus(Vec2.of(3, 4))).objEq(Vec2.of(4, 6)); 11 | expect(Vec2.of(1, 2).plus(Vec3.of(3, 4, 1))).objEq(Vec3.of(4, 6, 1)); 12 | }); 13 | 14 | it('Multiplication', () => { 15 | expect(Vec2.of(1, -1).times(22)).objEq(Vec2.of(22, -22)); 16 | expect(Vec2.of(1, -1).scale(Vec3.of(-1, 2, 3))).objEq(Vec2.of(-1, -2)); 17 | }); 18 | 19 | it('More complicated expressions', () => { 20 | expect(Vec2.of(1, 2).plus(Vec2.of(3, 4)).times(2)).objEq(Vec2.of(8, 12)); 21 | }); 22 | 23 | it('Angle', () => { 24 | expect(Vec2.of(-1, 1).angle()).toBeCloseTo((3 * Math.PI) / 4); 25 | }); 26 | 27 | it('Perpindicular', () => { 28 | const tolerance = 0.001; 29 | expect(Vec2.unitX.cross(Vec3.unitZ)).objEq(Vec2.unitY.times(-1), tolerance); 30 | expect(Vec2.unitX.orthog()).objEq(Vec2.unitY, tolerance); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/lib.ts: -------------------------------------------------------------------------------- 1 | export { default as InputMapper } from './InputFilter/InputMapper'; 2 | 3 | export { default as BaseTool } from './BaseTool'; 4 | export { default as ToolController } from './ToolController'; 5 | export { default as ToolEnabledGroup } from './ToolEnabledGroup'; 6 | 7 | export { default as UndoRedoShortcut } from './UndoRedoShortcut'; 8 | export { default as ToolSwitcherShortcut } from './ToolSwitcherShortcut'; 9 | export { default as PanZoomTool, PanZoomMode } from './PanZoom'; 10 | 11 | export { default as PenTool, PenStyle } from './Pen'; 12 | export { default as TextTool } from './TextTool'; 13 | export { default as SelectionTool, SelectionMode } from './SelectionTool/SelectionTool'; 14 | export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler'; 15 | export { default as EraserTool, EraserMode } from './Eraser'; 16 | export { default as PasteHandler } from './PasteHandler'; 17 | export { default as SoundUITool } from './SoundUITool'; 18 | 19 | export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler'; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "target": "ES2021", 5 | "module": "CommonJS", 6 | "outDir": "./dist/cjs", 7 | 8 | "forceConsistentCasingInFileNames": true, 9 | "listEmittedFiles": false, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noImplicitOverride": true, 14 | "noUnusedLocals": true, 15 | 16 | "strictBindCallApply": true, 17 | "strictFunctionTypes": true, 18 | "strictNullChecks": true, 19 | "esModuleInterop": true, 20 | "moduleResolution": "Node", 21 | "declaration": true, 22 | 23 | "paths": { 24 | "js-draw": ["./packages/js-draw/src/lib.ts"], 25 | "@js-draw/*": ["./packages/*/src/lib.ts"] 26 | } 27 | }, 28 | "exclude": [ 29 | "**/node_modules", 30 | 31 | // Files that don't need transpilation 32 | // "**/*.test.ts", <- vscode requires .test.ts files to be transpiled for other settings to apply. 33 | "./testing/__mocks__/*", 34 | 35 | // Output files 36 | "./dist/**" 37 | ], 38 | 39 | "files": ["./testing/global.d.ts"] 40 | } 41 | -------------------------------------------------------------------------------- /packages/material-icons/src/icons/ScreenLockRotation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/math/src/rounding/cleanUpNumber.ts: -------------------------------------------------------------------------------- 1 | /** Cleans up stringified numbers */ 2 | export const cleanUpNumber = (text: string) => { 3 | // Regular expression substitions can be somewhat expensive. Only do them 4 | // if necessary. 5 | 6 | if (text.indexOf('e') > 0) { 7 | // Round to zero. 8 | if (text.match(/[eE][-]\d{2,}$/)) { 9 | return '0'; 10 | } 11 | } 12 | 13 | const lastChar = text.charAt(text.length - 1); 14 | if (lastChar === '0' || lastChar === '.') { 15 | // Remove trailing zeroes 16 | text = text.replace(/([.]\d*[^0])0+$/, '$1'); 17 | text = text.replace(/[.]0+$/, '.'); 18 | 19 | // Remove trailing period 20 | text = text.replace(/[.]$/, ''); 21 | } 22 | 23 | const firstChar = text.charAt(0); 24 | if (firstChar === '0' || firstChar === '-') { 25 | // Remove unnecessary leading zeroes. 26 | text = text.replace(/^(0+)[.]/, '.'); 27 | text = text.replace(/^-(0+)[.]/, '-.'); 28 | text = text.replace(/^(-?)0+$/, '$10'); 29 | } 30 | 31 | if (text === '-0') { 32 | return '0'; 33 | } 34 | 35 | return text; 36 | }; 37 | 38 | export default cleanUpNumber; 39 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/src/browser/iframe.scss: -------------------------------------------------------------------------------- 1 | :root.console-view { 2 | background-color: #222; 3 | color: white; 4 | 5 | body > div { 6 | margin-bottom: 10px; 7 | 8 | font-family: monospace; 9 | white-space: pre-wrap; 10 | 11 | & > details { 12 | display: inline-block; 13 | } 14 | 15 | & > * { 16 | margin-right: 5px; 17 | } 18 | 19 | &.error { 20 | color: red; 21 | } 22 | 23 | &.warning { 24 | color: yellow; 25 | } 26 | 27 | .color-square { 28 | width: 1em; 29 | height: 1em; 30 | margin-right: 3px; 31 | display: inline-block; 32 | 33 | border: 1px solid white; 34 | } 35 | 36 | .matrix-output { 37 | display: inline-block; 38 | color: #ffecff; 39 | } 40 | } 41 | 42 | details { 43 | & > summary { 44 | cursor: pointer; 45 | } 46 | 47 | &[open] { 48 | padding: 2px; 49 | } 50 | 51 | transition: 0.2s ease padding; 52 | } 53 | } 54 | 55 | :root.html-view { 56 | .log-item.error { 57 | font-family: monospace; 58 | color: red; 59 | } 60 | 61 | scrollbar-gutter: stable; 62 | scrollbar-width: thin; 63 | } 64 | -------------------------------------------------------------------------------- /docs/demo/ui/loadFromSaveList.css: -------------------------------------------------------------------------------- 1 | .save-item-list { 2 | display: flex; 3 | flex-direction: column-reverse; 4 | 5 | max-width: 500px; 6 | margin-left: auto; 7 | margin-right: auto; 8 | } 9 | 10 | .save-item { 11 | display: flex; 12 | justify-content: space-between; 13 | flex-direction: row; 14 | 15 | max-height: 120px; 16 | 17 | border-bottom: 1px solid gray; 18 | 19 | background-color: #eee; 20 | --icon-color: black; 21 | } 22 | 23 | .save-item button { 24 | background-color: transparent; 25 | color: inherit; 26 | border: none; 27 | } 28 | 29 | .save-item .icon { 30 | max-height: 100%; 31 | width: 50px; 32 | } 33 | 34 | .save-item button:hover { 35 | background-color: #aaa; 36 | } 37 | 38 | .save-item .open-button { 39 | flex-grow: 1; 40 | 41 | display: flex; 42 | flex-direction: row; 43 | justify-content: start; 44 | align-items: center; 45 | font-size: 1.5rem; 46 | } 47 | 48 | .save-item-list { 49 | margin-bottom: 30px; 50 | } 51 | 52 | @media (prefers-color-scheme: dark) { 53 | .save-item { 54 | background-color: #333; 55 | color: white; 56 | --icon-color: white; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/demo/icons/js-draw-icon-vector-large.svg: -------------------------------------------------------------------------------- 1 | JS-DrawJS-Draw 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // Test configuration 2 | // See https://jestjs.io/docs/configuration#testenvironment-string 3 | 4 | const config = { 5 | preset: 'ts-jest', 6 | 7 | // File extensions for imports, in order of precedence: 8 | moduleFileExtensions: ['ts', 'js'], 9 | 10 | testPathIgnorePatterns: ['/dist/', '/node_modules/'], 11 | 12 | // Mocks. 13 | // See https://jestjs.io/docs/webpack#handling-static-assets 14 | moduleNameMapper: { 15 | // Webpack/ESBuild allows importing CSS files. Mock it. 16 | '\\.(css|lessc)': '/testing/mocks/styleMock.js', 17 | '@melloware/coloris': '/testing/mocks/coloris.ts', 18 | }, 19 | 20 | testEnvironment: 'jsdom', 21 | testEnvironmentOptions: { 22 | // Prevents scripts from running within iframes (including sandboxed iframes) 23 | // which prevents "Error: The SVG sandbox is broken! Please double-check the sandboxing setting." 24 | // from being repeatedly logged to the console during testing. 25 | runScripts: 'outside-only', 26 | }, 27 | setupFilesAfterEnv: ['/testing/beforeEachFile.ts'], 28 | }; 29 | 30 | module.exports = config; 31 | -------------------------------------------------------------------------------- /packages/js-draw/src/testing/sendPenEvent.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../Editor'; 2 | import { Point2 } from '@js-draw/math'; 3 | import Pointer, { PointerDevice } from '../Pointer'; 4 | import { InputEvtType, PointerEvtType } from '../inputEvents'; 5 | import getUniquePointerId from './getUniquePointerId'; 6 | 7 | /** 8 | * Dispatch a pen event to the currently selected tool. 9 | * Intended for unit tests. 10 | * 11 | * @see {@link sendTouchEvent} 12 | */ 13 | const sendPenEvent = ( 14 | editor: Editor, 15 | eventType: PointerEvtType, 16 | point: Point2, 17 | 18 | allPointers?: Pointer[], 19 | 20 | deviceType: PointerDevice = PointerDevice.Pen, 21 | ) => { 22 | const id = getUniquePointerId(allPointers ?? []); 23 | 24 | const mainPointer = Pointer.ofCanvasPoint( 25 | point, 26 | eventType !== InputEvtType.PointerUpEvt, 27 | editor.viewport, 28 | id, 29 | deviceType, 30 | ); 31 | 32 | editor.toolController.dispatchInputEvent({ 33 | kind: eventType, 34 | allPointers: allPointers ?? [mainPointer], 35 | current: mainPointer, 36 | }); 37 | 38 | return mainPointer; 39 | }; 40 | export default sendPenEvent; 41 | -------------------------------------------------------------------------------- /docs/debugging/undo-history-visualizer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Debugging | js-draw examples 7 | 43 | 44 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Henry Heino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | 'on': pull_request 6 | 7 | # See https://github.com/FirebaseExtended/action-hosting-deploy/issues/198 8 | # and https://github.com/FirebaseExtended/action-hosting-deploy/issues/108#issuecomment-1036024111 9 | permissions: 10 | contents: read 11 | pull-requests: write 12 | checks: write 13 | 14 | jobs: 15 | build_and_preview: 16 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v6 20 | - name: Prepare yarn 21 | run: corepack enable 22 | - name: Install dependencies 23 | run: yarn install 24 | - name: Build 25 | run: bash ./scripts/build-website.sh 26 | - uses: FirebaseExtended/action-hosting-deploy@v0 27 | with: 28 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 29 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_JS_DRAW }}' 30 | projectId: js-draw 31 | -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | js-draw 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

18 | js-draw 19 |
20 | 21 | API 22 |

23 |
24 |
25 | 26 | 27 | 32 | 33 | -------------------------------------------------------------------------------- /packages/js-draw/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Henry Heino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/math/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Henry Heino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/demo/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "JS-draw", 3 | "name": "JS-draw: Demo", 4 | "icons": [ 5 | { 6 | "src": "./icons/js-draw-icon-vector-large.svg", 7 | "type": "image/svg+xml", 8 | "sizes": "512x512", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "./icons/js-draw-icon-vector-medium.svg", 13 | "type": "image/svg+xml", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "./icons/js-draw-icon-large.png", 18 | "type": "image/png", 19 | "sizes": "512x512" 20 | }, 21 | { 22 | "src": "./icons/js-draw-icon-medium.png", 23 | "type": "image/png", 24 | "sizes": "192x192" 25 | }, 26 | { 27 | "src": "./icons/js-draw-icon-small.png", 28 | "type": "image/png", 29 | "sizes": "64x64" 30 | } 31 | ], 32 | "file_handlers": [ 33 | { 34 | "action": "./", 35 | "accept": { 36 | "image/svg+xml": [".svg"] 37 | } 38 | } 39 | ], 40 | "start_url": "./", 41 | "background_color": "#853360", 42 | "theme_color": "#803380", 43 | "display": "standalone", 44 | "screenshots": [ 45 | { 46 | "src": "../img/js-draw.jpg", 47 | "type": "image/jpg", 48 | "sizes": "1668x979" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /packages/debugging/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Henry Heino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/InsertImageWidget/InsertImageWidget.scss: -------------------------------------------------------------------------------- 1 | $image-widget-selector: '.insert-image-widget-dropdown-content'; 2 | // Repeat the selector for additional specificity. 3 | :root #{$image-widget-selector}#{$image-widget-selector}#{$image-widget-selector} { 4 | & > div > div { 5 | padding: 5px; 6 | } 7 | 8 | & > div { 9 | min-height: 0; 10 | } 11 | 12 | img { 13 | max-width: 100%; 14 | max-height: 100%; 15 | 16 | /* Center */ 17 | display: block; 18 | margin-left: auto; 19 | margin-right: auto; 20 | } 21 | 22 | .insert-image-image-status-view { 23 | display: flex; 24 | justify-content: space-between; 25 | padding-bottom: 0; 26 | } 27 | 28 | .action-button-row { 29 | margin-top: 4px; 30 | display: flex; 31 | flex-direction: row; 32 | justify-content: flex-end; 33 | 34 | // Ensure that (on toolbars that support it), the action button 35 | // is touching the bottom edge of the screen. 36 | padding-bottom: 0; 37 | margin-bottom: 0; 38 | } 39 | 40 | .action-button-row > button { 41 | flex-grow: 1; 42 | text-align: end; 43 | max-width: 50%; 44 | min-width: min(100%, 40px); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/js-draw/src/components/lib.ts: -------------------------------------------------------------------------------- 1 | export * from './builders/types'; 2 | 3 | export * from './builders/lib'; 4 | export { default as StrokeSmoother, Curve as StrokeSmootherCurve } from './util/StrokeSmoother'; 5 | 6 | export * from './AbstractComponent'; 7 | export { default as AbstractComponent } from './AbstractComponent'; 8 | import Stroke from './Stroke'; 9 | import TextComponent from './TextComponent'; 10 | import ImageComponent from './ImageComponent'; 11 | import RestyleableComponent from './RestylableComponent'; 12 | import { 13 | createRestyleComponentCommand, 14 | isRestylableComponent, 15 | ComponentStyle as RestyleableComponentStyle, 16 | } from './RestylableComponent'; 17 | import BackgroundComponent, { BackgroundType } from './BackgroundComponent'; 18 | 19 | export { 20 | Stroke, 21 | RestyleableComponent, 22 | createRestyleComponentCommand, 23 | isRestylableComponent, 24 | RestyleableComponentStyle, 25 | TextComponent, 26 | 27 | /** @deprecated use {@link TextComponent} */ 28 | TextComponent as Text, 29 | Stroke as StrokeComponent, 30 | BackgroundComponent, 31 | BackgroundType as BackgroundComponentBackgroundType, 32 | ImageComponent, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/material-icons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/material-icons", 3 | "version": "1.32.0", 4 | "description": "Material icon pack for js-draw. ", 5 | "types": "./dist/mjs/lib.d.ts", 6 | "main": "./dist/cjs/lib.js", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/mjs/lib.d.ts", 10 | "import": "./dist/mjs/lib.mjs", 11 | "require": "./dist/cjs/lib.js" 12 | }, 13 | "./bundle": { 14 | "types": "./dist/mjs/bundle.d.ts", 15 | "default": "./dist/bundle.js" 16 | } 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/personalizedrefrigerator/js-draw.git" 21 | }, 22 | "author": "Henry Heino", 23 | "license": "(MIT AND Apache-2.0)", 24 | "scripts": { 25 | "dist": "yarn run build", 26 | "build": "build-tool build", 27 | "watch": "build-tool watch" 28 | }, 29 | "devDependencies": { 30 | "@js-draw/build-tool": "^1.32.0", 31 | "js-draw": "^1.32.0" 32 | }, 33 | "peerDependencies": { 34 | "js-draw": "^1.0.1" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/personalizedrefrigerator/js-draw/issues" 38 | }, 39 | "homepage": "https://github.com/personalizedrefrigerator/js-draw#readme" 40 | } 41 | -------------------------------------------------------------------------------- /packages/debugging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/debugging", 3 | "version": "1.32.0", 4 | "description": "A debugging/development library for js-draw.", 5 | "types": "./dist/mjs/lib.d.ts", 6 | "main": "./dist/cjs/lib.js", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/mjs/lib.d.ts", 10 | "import": "./dist/mjs/lib.mjs", 11 | "require": "./dist/cjs/lib.js" 12 | } 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/personalizedrefrigerator/js-draw.git" 17 | }, 18 | "author": "Henry Heino", 19 | "license": "MIT", 20 | "private": true, 21 | "scripts": { 22 | "dist": "yarn run build", 23 | "build": "build-tool build", 24 | "watch": "build-tool watch" 25 | }, 26 | "devDependencies": { 27 | "@js-draw/build-tool": "^1.32.0", 28 | "js-draw": "^1.32.0" 29 | }, 30 | "peerDependencies": { 31 | "js-draw": "^1.8.0" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/personalizedrefrigerator/js-draw/issues" 35 | }, 36 | "homepage": "https://github.com/personalizedrefrigerator/js-draw#readme", 37 | "keywords": [ 38 | "ink", 39 | "drawing", 40 | "pen", 41 | "freehand", 42 | "svg" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/SelectionTool/SelectAllShortcutHandler.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../Editor'; 2 | import { KeyPressEvent } from '../../inputEvents'; 3 | import BaseTool from '../BaseTool'; 4 | import { selectAllKeyboardShortcut } from '../keybindings'; 5 | import SelectionTool from './SelectionTool'; 6 | 7 | // Handles ctrl+a: Select all 8 | export default class SelectAllShortcutHandler extends BaseTool { 9 | public constructor(private editor: Editor) { 10 | super(editor.notifier, editor.localization.selectAllTool); 11 | } 12 | 13 | public override canReceiveInputInReadOnlyEditor() { 14 | return true; 15 | } 16 | 17 | // @internal 18 | public override onKeyPress(event: KeyPressEvent): boolean { 19 | if (this.editor.shortcuts.matchesShortcut(selectAllKeyboardShortcut, event)) { 20 | const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool); 21 | 22 | if (selectionTools.length > 0) { 23 | const selectionTool = selectionTools[0]; 24 | selectionTool.setEnabled(true); 25 | selectionTool.setSelection(this.editor.image.getAllComponents()); 26 | 27 | return true; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/demo/storage/AbstractStore.ts: -------------------------------------------------------------------------------- 1 | import ImageSaver from './ImageSaver'; 2 | 3 | /** 4 | * Represents an image and its metadata. Such an entry implements 5 | * ImageSaver so that changes can be made to the image, its title, etc. 6 | */ 7 | export interface StoreEntry extends ImageSaver { 8 | /** The name of the image */ 9 | title: string; 10 | 11 | /** A prevew image that can be shown before opening the full image. */ 12 | getIcon(): Element; 13 | 14 | /** Delete the image and its metadata. */ 15 | delete(): Promise; 16 | 17 | /** Overwrite the image with new content. */ 18 | write(newContent: string): Promise; 19 | 20 | /** Read the content of the image. */ 21 | read(): Promise; 22 | } 23 | 24 | /** 25 | * An `interface` to be implemented by all methods of saving/retreiving 26 | * multiple images. 27 | * 28 | * Instances of this `interface` provide a way of reading/writing/deleting 29 | * images. 30 | */ 31 | interface AbstractStore { 32 | getEntries(): Promise; 33 | 34 | /** Creates a new `StoreEntry` or, on error, returns `null`. */ 35 | createNewEntry(): Promise; 36 | } 37 | 38 | export default AbstractStore; 39 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/util/createMenuOverlay.test.ts: -------------------------------------------------------------------------------- 1 | import { Vec2 } from '@js-draw/math'; 2 | import createEditor from '../../testing/createEditor'; 3 | import createMenuOverlay from './createMenuOverlay'; 4 | import findNodeWithText from '../../testing/findNodeWithText'; 5 | import firstElementAncestorOfNode from '../../testing/firstElementAncestorOfNode'; 6 | 7 | describe('createMenuOverlay', () => { 8 | test('should return the key for the clicked item', async () => { 9 | const editor = createEditor(); 10 | const result = createMenuOverlay(editor, Vec2.zero, [ 11 | { 12 | key: 'test', 13 | text: 'Item to be selected', 14 | icon: () => editor.icons.makeCopyIcon(), 15 | }, 16 | { 17 | key: 'test2', 18 | text: 'Some other item', 19 | icon: () => editor.icons.makePasteIcon(), 20 | }, 21 | ]); 22 | 23 | const target = firstElementAncestorOfNode( 24 | findNodeWithText('Item to be selected', editor.getRootElement()), 25 | ); 26 | if (!target) { 27 | throw new Error('Unable to find target item'); 28 | } 29 | 30 | target.click(); 31 | await jest.runAllTimersAsync(); 32 | await expect(result).resolves.toBe('test'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/DropdownToolbar.scss: -------------------------------------------------------------------------------- 1 | .toolbar-dropdown-toolbar { 2 | button, 3 | .toolbar-button { 4 | background-color: var(--background-color-2); 5 | color: var(--foreground-color-2); 6 | --icon-color: var(--foreground-color-2); 7 | } 8 | 9 | &, 10 | .toolbar-dropdown { 11 | background-color: var(--background-color-3); 12 | color: var(--foreground-color-3); 13 | } 14 | 15 | .toolbar-spacedList > div { 16 | // Add spacing between labels and inputs (assumes labels come first) 17 | & > label { 18 | padding-right: 10px; 19 | min-width: 50px; 20 | } 21 | } 22 | 23 | // Override the styling of color fields 24 | .clr-field { 25 | // Make the preview a rounded box. 26 | button { 27 | width: 100%; 28 | height: 100%; 29 | 30 | // Ensure that the preview is centered vertically 31 | top: 50%; 32 | left: 0; 33 | 34 | border-radius: 5px; 35 | } 36 | } 37 | 38 | .toolbar-grid-selector > div { 39 | --button-size: 57px; 40 | } 41 | 42 | // Show toolbar buttons within dropdowns from left to right 43 | // rather than from top to bottom. 44 | .toolbar-dropdown > div > .toolbar-toolContainer { 45 | display: inline-block; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/js-draw/src/commands/UnresolvedCommand.ts: -------------------------------------------------------------------------------- 1 | import EditorImage from '../image/EditorImage'; 2 | import AbstractComponent from '../components/AbstractComponent'; 3 | import SerializableCommand from './SerializableCommand'; 4 | 5 | export type ResolveFromComponentCallback = () => SerializableCommand; 6 | 7 | /** 8 | * A command that requires a component that may or may not be present in the editor when 9 | * the command is created. 10 | */ 11 | export default abstract class UnresolvedSerializableCommand extends SerializableCommand { 12 | protected component: AbstractComponent | null; 13 | protected readonly componentID: string; 14 | 15 | protected constructor(commandId: string, componentID: string, component?: AbstractComponent) { 16 | super(commandId); 17 | this.component = component ?? null; 18 | this.componentID = componentID; 19 | } 20 | 21 | protected resolveComponent(image: EditorImage) { 22 | if (this.component) { 23 | return; 24 | } 25 | 26 | const component = image.lookupElement(this.componentID); 27 | if (!component) { 28 | throw new Error(`Unable to resolve component with ID ${this.componentID}`); 29 | } 30 | 31 | this.component = component; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/math/src/polynomial/solveQuadratic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Solves an equation of the form ax² + bx + c = 0. 3 | * The larger solution is returned first. 4 | * 5 | * If there are no solutions, returns `[NaN, NaN]`. If there is one solution, 6 | * repeats the solution twice in the result. 7 | */ 8 | const solveQuadratic = (a: number, b: number, c: number): [number, number] => { 9 | // See also https://en.wikipedia.org/wiki/Quadratic_formula 10 | 11 | if (a === 0) { 12 | let solution; 13 | 14 | if (b === 0) { 15 | solution = c === 0 ? 0 : NaN; 16 | } else { 17 | // Then we have bx + c = 0 18 | // which implies bx = -c. 19 | // Thus, x = -c/b 20 | solution = -c / b; 21 | } 22 | 23 | return [solution, solution]; 24 | } 25 | 26 | const discriminant = b * b - 4 * a * c; 27 | 28 | if (discriminant < 0) { 29 | return [NaN, NaN]; 30 | } 31 | 32 | const rootDiscriminant = Math.sqrt(discriminant); 33 | const solution1 = (-b + rootDiscriminant) / (2 * a); 34 | const solution2 = (-b - rootDiscriminant) / (2 * a); 35 | 36 | if (solution1 > solution2) { 37 | return [solution1, solution2]; 38 | } else { 39 | return [solution2, solution1]; 40 | } 41 | }; 42 | export default solveQuadratic; 43 | -------------------------------------------------------------------------------- /packages/js-draw/src/SVGLoader/SVGLoader.plugins.test.ts: -------------------------------------------------------------------------------- 1 | import { Color4 } from '@js-draw/math'; 2 | import { Stroke } from '../lib'; 3 | import SVGLoader, { SVGLoaderPlugin } from './SVGLoader'; 4 | 5 | describe('SVGLoader.plugins', () => { 6 | test('should support custom plugin callbacks', async () => { 7 | let visitCount = 0; 8 | let skipCount = 0; 9 | const plugin: SVGLoaderPlugin = { 10 | async visit(node, control) { 11 | if (node.hasAttribute('data-test')) { 12 | control.addComponent(Stroke.fromFilled('m0,0 l10,10 l-10,0 z', Color4.red)); 13 | visitCount++; 14 | return true; 15 | } else { 16 | skipCount++; 17 | } 18 | return false; 19 | }, 20 | }; 21 | 22 | const loader = SVGLoader.fromString( 23 | ` 24 | 25 | Testing... 26 | Test 2... 27 | Test 2... 28 | 29 | `, 30 | { 31 | plugins: [plugin], 32 | }, 33 | ); 34 | const onAddListener = jest.fn(); 35 | const onProgressListener = jest.fn(); 36 | await loader.start(onAddListener, onProgressListener); 37 | 38 | expect(visitCount).toBe(2); 39 | expect(skipCount).toBeGreaterThanOrEqual(1); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/js-draw/src/rendering/caching/types.ts: -------------------------------------------------------------------------------- 1 | import type { Vec2 } from '@js-draw/math'; 2 | import type AbstractRenderer from '../renderers/AbstractRenderer'; 3 | import type { CacheRecordManager } from './CacheRecordManager'; 4 | 5 | export type CacheAddress = number; 6 | export type BeforeDeallocCallback = () => void; 7 | 8 | export interface CacheProps { 9 | createRenderer(): AbstractRenderer; 10 | // Returns whether the cache can be rendered onto [renderer]. 11 | isOfCorrectType(renderer: AbstractRenderer): boolean; 12 | 13 | blockResolution: Vec2; 14 | cacheSize: number; 15 | 16 | // Maximum amount a cached image can be scaled without a re-render 17 | // (larger numbers = blurrier, but faster) 18 | maxScale: number; 19 | 20 | // Minimum component count to cache, rather than just re-render each time. 21 | minProportionalRenderTimePerCache: number; 22 | 23 | // Minimum number of strokes/etc. to use the cache to render, isntead of 24 | // rendering directly. 25 | minProportionalRenderTimeToUseCache: number; 26 | } 27 | 28 | export interface CacheState { 29 | currentRenderingCycle: number; 30 | props: CacheProps; 31 | recordManager: CacheRecordManager; 32 | 33 | // @internal 34 | debugMode: boolean; 35 | } 36 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/dom/addLongPressOrHoverCssClasses.ts: -------------------------------------------------------------------------------- 1 | import listenForLongPressOrHover from './listenForLongPressOrHover'; 2 | 3 | /** 4 | * When a pointer is inside `element`, after a delay, adds the `has-long-press-or-hover` 5 | * CSS class to `element`. 6 | * 7 | * When no pointers are inside `element`, adds the CSS class `no-long-press-or-hover`. 8 | */ 9 | const addLongPressOrHoverCssClasses = (element: HTMLElement, options?: { timeout: number }) => { 10 | const hasLongPressClass = 'has-long-press-or-hover'; 11 | const noLongPressClass = 'no-long-press-or-hover'; 12 | 13 | element.classList.add('no-long-press-or-hover'); 14 | 15 | const { removeListeners } = listenForLongPressOrHover(element, { 16 | onStart() { 17 | element.classList.remove(noLongPressClass); 18 | element.classList.add(hasLongPressClass); 19 | }, 20 | onEnd() { 21 | element.classList.add(noLongPressClass); 22 | element.classList.remove(hasLongPressClass); 23 | }, 24 | longPressTimeout: options?.timeout, 25 | }); 26 | 27 | return { 28 | removeEventListeners: () => { 29 | element.classList.remove(noLongPressClass); 30 | removeListeners(); 31 | }, 32 | }; 33 | }; 34 | 35 | export default addLongPressOrHoverCssClasses; 36 | -------------------------------------------------------------------------------- /packages/js-draw/src/rendering/caching/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { Vec2 } from '@js-draw/math'; 2 | import DummyRenderer from '../renderers/DummyRenderer'; 3 | import createEditor from '../../testing/createEditor'; 4 | import AbstractRenderer from '../renderers/AbstractRenderer'; 5 | import RenderingCache from './RenderingCache'; 6 | import { CacheProps } from './types'; 7 | 8 | type RenderAllocCallback = (renderer: DummyRenderer) => void; 9 | 10 | // Override any default test options with [cacheOptions] 11 | export const createCache = ( 12 | onRenderAlloc?: RenderAllocCallback, 13 | cacheOptions?: Partial, 14 | ) => { 15 | const editor = createEditor(); 16 | 17 | const cache = new RenderingCache({ 18 | createRenderer() { 19 | const renderer = new DummyRenderer(editor.viewport); 20 | onRenderAlloc?.(renderer); 21 | return renderer; 22 | }, 23 | isOfCorrectType(renderer: AbstractRenderer) { 24 | return renderer instanceof DummyRenderer; 25 | }, 26 | blockResolution: Vec2.of(500, 500), 27 | cacheSize: 500 * 10 * 4, 28 | maxScale: 2, 29 | minProportionalRenderTimePerCache: 0, 30 | minProportionalRenderTimeToUseCache: 0, 31 | ...cacheOptions, 32 | }); 33 | 34 | return { 35 | cache, 36 | editor, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/build-tool/src/types.ts: -------------------------------------------------------------------------------- 1 | export type BuildMode = 'build' | 'watch'; 2 | 3 | export interface BundledFileRecord { 4 | name: string; 5 | inPath: string; 6 | 7 | // outPath defaults to a path based on inPath 8 | outPath?: string; 9 | } 10 | 11 | export interface TranslationSourcePair { 12 | // Name of the project 13 | name: string; 14 | 15 | // JavaScript source file providing the translation source. 16 | // Each file should have a single default export that contains 17 | // a map from locale keys (e.g. en for English, es for Español, etc.) to 18 | // a localization. 19 | path: string; 20 | 21 | // The key of the default locale (which should have translations for all valid 22 | // keys). 23 | defaultLocale: string; 24 | } 25 | 26 | export interface BuildConfig { 27 | bundledFiles: BundledFileRecord[]; 28 | 29 | prebuild: { 30 | // A path to a script to be run just before building 31 | scriptPath: string; 32 | } | null; 33 | 34 | translationSourceFiles: TranslationSourcePair[]; 35 | translationDestPath: string; 36 | 37 | // Paths to SCSS files to be compiled. If given, both inDirectory and 38 | // outDirectory must be specified. 39 | scssFiles: string[]; 40 | 41 | inDirectory: string | undefined; 42 | outDirectory: string | undefined; 43 | } 44 | -------------------------------------------------------------------------------- /packages/typedoc-extensions/src/CustomTheme.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme, NavigationElement, ProjectReflection, Renderer } from 'typedoc'; 2 | import loadRendererHooks from './loadRendererHooks'; 3 | 4 | // See https://github.com/TypeStrong/typedoc/blob/master/internal-docs/custom-themes.md 5 | 6 | class CustomTheme extends DefaultTheme { 7 | public constructor(renderer: Renderer) { 8 | super(renderer); 9 | 10 | loadRendererHooks(renderer, this.application.options); 11 | } 12 | 13 | public override buildNavigation(project: ProjectReflection): NavigationElement[] { 14 | const options = this.application.options; 15 | const sidebarReplacements = options.getValue('sidebarReplacements') as Record; 16 | const defaultNavigation = super.buildNavigation(project); 17 | 18 | const updateNavigationElement = (element: NavigationElement) => { 19 | if (element.children) { 20 | element.children.forEach(updateNavigationElement); 21 | } 22 | 23 | if (element.text in sidebarReplacements) { 24 | element.text = sidebarReplacements[element.text]; 25 | } 26 | }; 27 | 28 | for (const elem of defaultNavigation) { 29 | updateNavigationElement(elem); 30 | } 31 | 32 | return defaultNavigation; 33 | } 34 | } 35 | 36 | export default CustomTheme; 37 | -------------------------------------------------------------------------------- /packages/js-draw/src/localizations/getLocalizationTable.test.ts: -------------------------------------------------------------------------------- 1 | import { defaultEditorLocalization } from '../localization'; 2 | import en from './en'; 3 | import es from './es'; 4 | import getLocalizationTable from './getLocalizationTable'; 5 | 6 | describe('getLocalizationTable', () => { 7 | it('should return the en localization for es_TEST', () => { 8 | expect(getLocalizationTable(['es_TEST']) === es).toBe(true); 9 | }); 10 | 11 | it('should return the default localization for unsupported language', () => { 12 | expect(getLocalizationTable(['test']) === defaultEditorLocalization).toBe(true); 13 | }); 14 | 15 | it('should return the first localization matching a language in the list of user locales', () => { 16 | expect( 17 | getLocalizationTable([ 18 | 'test_TEST1', 19 | 'test_TEST2', 20 | 'test_TEST3', 21 | 'en_TEST', 22 | 'notalanguage', 23 | ]) === en, 24 | ).toBe(true); 25 | }); 26 | 27 | it('should return the default localization for unsupported language', () => { 28 | expect(getLocalizationTable(['test']) === defaultEditorLocalization).toBe(true); 29 | }); 30 | 31 | it("should return first of user's supported languages", () => { 32 | expect(getLocalizationTable(['es_MX', 'es_ES', 'en_US']) === es).toBe(true); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/js-draw/src/Editor.loadFrom.test.ts: -------------------------------------------------------------------------------- 1 | import { Color4 } from '@js-draw/math'; 2 | import { imageBackgroundCSSClassName } from './components/BackgroundComponent'; 3 | import { RestyleableComponent } from './lib'; 4 | import SVGLoader from './SVGLoader/SVGLoader'; 5 | import createEditor from './testing/createEditor'; 6 | 7 | describe('Editor.loadFrom', () => { 8 | it('should remove existing BackgroundComponents when loading new BackgroundComponents', async () => { 9 | const editor = createEditor(); 10 | await editor.dispatch(editor.setBackgroundColor(Color4.red)); 11 | 12 | let backgroundComponents = editor.image.getBackgroundComponents(); 13 | expect(backgroundComponents).toHaveLength(1); 14 | expect((backgroundComponents[0] as RestyleableComponent).getStyle().color).objEq(Color4.red); 15 | 16 | await editor.loadFrom( 17 | SVGLoader.fromString( 18 | ` 19 | 20 | `, 21 | true, 22 | ), 23 | ); 24 | 25 | backgroundComponents = editor.image.getBackgroundComponents(); 26 | expect(backgroundComponents).toHaveLength(1); 27 | expect((backgroundComponents[0] as RestyleableComponent).getStyle().color).objEq(Color4.black); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/TextToolWidget.test.ts: -------------------------------------------------------------------------------- 1 | import { makeEdgeToolbar } from '../EdgeToolbar'; 2 | import TextToolWidget from './TextToolWidget'; 3 | import createEditor from '../../testing/createEditor'; 4 | import { TextTool } from '../../lib'; 5 | import sendKeyPressRelease from '../../testing/sendKeyPressRelease'; 6 | 7 | describe('TextToolWidget', () => { 8 | test.each([[['sans', 'sans-serif', 'somefont']], [['free mono']]])( 9 | 'should use the list of fonts provided to the Editor constructor', 10 | async (fonts) => { 11 | const editor = createEditor({ 12 | text: { fonts }, 13 | }); 14 | const toolbar = makeEdgeToolbar(editor); 15 | 16 | const textTool = editor.toolController.getMatchingTools(TextTool)[0]; 17 | textTool.setEnabled(true); 18 | 19 | const textWidget = new TextToolWidget(editor, textTool); 20 | toolbar.addWidget(textWidget); 21 | 22 | const pressSpace = () => sendKeyPressRelease(editor, ' '); 23 | pressSpace(); 24 | 25 | const fontSelectors = editor.getRootElement().querySelectorAll('.font-selector'); 26 | expect(fontSelectors).toHaveLength(1); 27 | 28 | const itemValues = [...fontSelectors[0].querySelectorAll('option')].map((item) => item.value); 29 | expect(itemValues).toMatchObject(fonts); 30 | }, 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /docs/doc-pages/inline-examples/adding-an-image-and-data-urls.md: -------------------------------------------------------------------------------- 1 | ```ts,runnable 2 | import { Editor, ImageComponent, Mat33 } from 'js-draw'; 3 | const editor = new Editor(document.body); 4 | 5 | // 6 | // Adding an image 7 | // 8 | const myHtmlImage = new Image(); 9 | myHtmlImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAAKklEQVQIW2Ns022zZGRgfPnz8s8HDQwN/xgZgKBDu0PuL8tf5d8/fz8FAOiDD1H2gfpGAAAAAElFTkSuQmCC'; 10 | 11 | const rotated45Degrees = Mat33.zRotation(Math.PI / 4); // A 45 degree = pi/4 radian rotation 12 | const scaledByFactorOf100 = Mat33.scaling2D(100); 13 | // Scale **and** rotate 14 | const transform = rotated45Degrees.rightMul(scaledByFactorOf100); 15 | 16 | const imageComponent = await ImageComponent.fromImage(myHtmlImage, transform); 17 | await editor.dispatch(editor.image.addComponent(imageComponent)); 18 | 19 | // 20 | // Make a new image from the editor itself (with editor.toDataURL) 21 | // 22 | const toolbar = editor.addToolbar(); 23 | toolbar.addActionButton('From editor', async () => { 24 | const dataUrl = editor.toDataURL(); 25 | const htmlImage = new Image(); 26 | htmlImage.src = dataUrl; 27 | 28 | const imageComponent = await ImageComponent.fromImage(htmlImage, Mat33.identity); 29 | await editor.addAndCenterComponents([ imageComponent ]); 30 | }); 31 | ``` 32 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Path, Point2, Rect2 } from '@js-draw/math'; 2 | import EditorImage from '../../../image/EditorImage'; 3 | import SelectionBuilder from './SelectionBuilder'; 4 | 5 | /** 6 | * Creates rectangle selections 7 | */ 8 | export default class RectSelectionBuilder extends SelectionBuilder { 9 | private rect: Rect2; 10 | 11 | public constructor(startPoint: Point2) { 12 | super(); 13 | 14 | this.rect = Rect2.fromCorners(startPoint, startPoint); 15 | } 16 | 17 | public onPointerMove(canvasPoint: Point2) { 18 | this.rect = this.rect.grownToPoint(canvasPoint); 19 | } 20 | 21 | public previewPath() { 22 | return Path.fromRect(this.rect); 23 | } 24 | 25 | public resolveInternal(image: EditorImage) { 26 | return image.getComponentsIntersecting(this.rect).filter((element) => { 27 | // Filter out the case where the selection rectangle is completely contained 28 | // within the element (and does not intersect it). 29 | // This is useful, for example, if a very large stroke is used as the background 30 | // for another drawing. This prevents the very large stroke from being selected 31 | // unless the selection touches one of its edges. 32 | return element.intersectsRect(this.rect); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/ToolSwitcherShortcut.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../Editor'; 2 | import { KeyPressEvent } from '../inputEvents'; 3 | import BaseTool from './BaseTool'; 4 | 5 | /** 6 | * Handles keyboard events used, by default, to select tools. By default, 7 | * 1 maps to the first primary tool, 2 to the second primary tool, ... . 8 | * 9 | * This is in the default set of {@link ToolController} tools. 10 | * 11 | */ 12 | export default class ToolSwitcherShortcut extends BaseTool { 13 | public constructor(private editor: Editor) { 14 | super(editor.notifier, editor.localization.changeTool); 15 | } 16 | 17 | public override canReceiveInputInReadOnlyEditor() { 18 | return true; 19 | } 20 | 21 | // @internal 22 | public override onKeyPress({ key }: KeyPressEvent): boolean { 23 | const toolController = this.editor.toolController; 24 | const primaryTools = toolController.getPrimaryTools(); 25 | 26 | // Map keys 0-9 to primary tools. 27 | const keyMatch = /^[0-9]$/.exec(key); 28 | 29 | let targetTool: BaseTool | undefined; 30 | if (keyMatch) { 31 | const targetIdx = parseInt(keyMatch[0], 10) - 1; 32 | targetTool = primaryTools[targetIdx]; 33 | } 34 | 35 | if (targetTool) { 36 | targetTool.setEnabled(true); 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/math/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-draw/math", 3 | "version": "1.32.0", 4 | "description": "A math library for js-draw. ", 5 | "types": "./dist/mjs/lib.d.ts", 6 | "main": "./dist/cjs/lib.js", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/mjs/lib.d.ts", 10 | "import": "./dist/mjs/lib.mjs", 11 | "require": "./dist/cjs/lib.js" 12 | } 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/personalizedrefrigerator/js-draw.git" 17 | }, 18 | "author": "Henry Heino", 19 | "license": "MIT", 20 | "scripts": { 21 | "dist-test": "cd dist-test/test_imports && yarn install && yarn run test", 22 | "dist": "yarn run build && yarn run dist-test", 23 | "build": "build-tool build", 24 | "watch": "build-tool watch" 25 | }, 26 | "dependencies": { 27 | "bezier-js": "6.1.3" 28 | }, 29 | "devDependencies": { 30 | "@js-draw/build-tool": "^1.32.0", 31 | "@types/bezier-js": "4.1.0", 32 | "@types/jest": "29.5.5", 33 | "@types/jsdom": "21.1.3" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/personalizedrefrigerator/js-draw/issues" 37 | }, 38 | "homepage": "https://github.com/personalizedrefrigerator/js-draw#readme", 39 | "keywords": [ 40 | "ink", 41 | "drawing", 42 | "pen", 43 | "freehand", 44 | "svg", 45 | "math" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/dom/stopPropagationOfScrollingWheelEvents.ts: -------------------------------------------------------------------------------- 1 | const stopPropagationOfScrollingWheelEvents = (scrollingContainer: HTMLElement) => { 2 | const scrollsAxis = ( 3 | delta: number, 4 | clientSize: number, 5 | scrollOffset: number, 6 | scrollSize: number, 7 | ) => { 8 | const hasScroll = clientSize !== scrollSize && delta !== 0; 9 | 10 | const eventScrollsPastStart = scrollOffset + delta <= 0; 11 | const scrollEnd = scrollOffset + clientSize; 12 | const eventScrollsPastEnd = scrollEnd + delta > scrollSize; 13 | 14 | return hasScroll && !eventScrollsPastStart && !eventScrollsPastEnd; 15 | }; 16 | 17 | scrollingContainer.onwheel = (event) => { 18 | const scrollsX = scrollsAxis( 19 | event.deltaX, 20 | scrollingContainer.clientWidth, 21 | scrollingContainer.scrollLeft, 22 | scrollingContainer.scrollWidth, 23 | ); 24 | const scrollsY = scrollsAxis( 25 | event.deltaY, 26 | scrollingContainer.clientHeight, 27 | scrollingContainer.scrollTop, 28 | scrollingContainer.scrollHeight, 29 | ); 30 | 31 | // Stop the editor from receiving the event if it will scroll the pen type selector 32 | // instead. 33 | if (scrollsX || scrollsY) { 34 | event.stopPropagation(); 35 | } 36 | }; 37 | }; 38 | 39 | export default stopPropagationOfScrollingWheelEvents; 40 | -------------------------------------------------------------------------------- /packages/js-draw/src/rendering/renderers/TextOnlyRenderer.test.ts: -------------------------------------------------------------------------------- 1 | import { ImageComponent, Mat33 } from '../../lib'; 2 | import createEditor from '../../testing/createEditor'; 3 | import TextOnlyRenderer from './TextOnlyRenderer'; 4 | 5 | describe('TextOnlyRenderer', () => { 6 | it('should summarize the number of visible image nodes', () => { 7 | const editor = createEditor(); 8 | 9 | const htmlImage = new Image(); 10 | htmlImage.width = 500; 11 | htmlImage.height = 200; 12 | 13 | const image = new ImageComponent({ 14 | transform: Mat33.identity, 15 | image: htmlImage, 16 | base64Url: '', 17 | label: 'Testing...', 18 | }); 19 | editor.dispatch(editor.image.addComponent(image)); 20 | 21 | const textRenderer = new TextOnlyRenderer(editor.viewport, editor.localization); 22 | editor.image.render(textRenderer, editor.viewport); 23 | 24 | // Should contian number of image nodes and the image description 25 | expect(textRenderer.getDescription()).toContain('Testing...'); 26 | expect(textRenderer.getDescription()).toContain('1'); 27 | 28 | textRenderer.clear(); 29 | 30 | // After clearing, should not contain description of the image/image count 31 | expect(textRenderer.getDescription()).not.toContain('Testing...'); 32 | expect(textRenderer.getDescription()).not.toContain('1'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/keybindings.ts: -------------------------------------------------------------------------------- 1 | import KeyboardShortcutManager from '../../shortcuts/KeyboardShortcutManager'; 2 | 3 | // Selection 4 | export const resizeImageToSelectionKeyboardShortcut = 5 | 'jsdraw.toolbar.SelectionTool.resizeImageToSelection'; 6 | KeyboardShortcutManager.registerDefaultKeyboardShortcut( 7 | resizeImageToSelectionKeyboardShortcut, 8 | ['ctrlOrMeta+r'], 9 | 'Resize image to selection', 10 | ); 11 | 12 | // Pen tool 13 | export const selectStrokeTypeKeyboardShortcutIds: string[] = [1, 2, 3, 4, 5, 6, 7, 8, 9].map( 14 | (id) => `jsdraw.toolbar.PenTool.select-pen-${id}`, 15 | ); 16 | 17 | for (let i = 0; i < selectStrokeTypeKeyboardShortcutIds.length; i++) { 18 | const id = selectStrokeTypeKeyboardShortcutIds[i]; 19 | KeyboardShortcutManager.registerDefaultKeyboardShortcut( 20 | id, 21 | [`CtrlOrMeta+Digit${i + 1}`], 22 | 'Select pen style ' + (i + 1), 23 | ); 24 | } 25 | 26 | // Save 27 | export const saveKeyboardShortcut = 'jsdraw.toolbar.SaveActionWidget.save'; 28 | KeyboardShortcutManager.registerDefaultKeyboardShortcut( 29 | saveKeyboardShortcut, 30 | ['ctrlOrMeta+KeyS'], 31 | 'Save', 32 | ); 33 | 34 | // Exit 35 | export const exitKeyboardShortcut = 'jsdraw.toolbar.ExitActionWidget.exit'; 36 | KeyboardShortcutManager.registerDefaultKeyboardShortcut(exitKeyboardShortcut, ['Alt+KeyQ'], 'Exit'); 37 | -------------------------------------------------------------------------------- /packages/js-draw/src/SVGLoader/utils/determineFontSize.ts: -------------------------------------------------------------------------------- 1 | /** Computes the font size of a text element, based on style information. */ 2 | const determineFontSize = ( 3 | elem: SVGTextElement | SVGTSpanElement, 4 | computedStyles: CSSStyleDeclaration | undefined, 5 | 6 | // output: Written to to update supported style attributes 7 | supportedStyleAttrs: Set, 8 | ) => { 9 | const fontSizeExp = /^([-0-9.e]+)px/i; 10 | 11 | // In some environments, computedStyles.fontSize can be increased by the system. 12 | // Thus, to prevent text from growing on load/save, prefer .style.fontSize. 13 | let fontSizeMatch = fontSizeExp.exec(elem.style?.fontSize ?? ''); 14 | if (!fontSizeMatch && elem.tagName.toLowerCase() === 'tspan' && elem.parentElement) { 15 | // Try to inherit the font size of the parent text element. 16 | fontSizeMatch = fontSizeExp.exec(elem.parentElement.style?.fontSize ?? ''); 17 | } 18 | 19 | // If we still couldn't find a font size, try to use computedStyles (which can be 20 | // wrong). 21 | if (!fontSizeMatch && computedStyles) { 22 | fontSizeMatch = fontSizeExp.exec(computedStyles.fontSize); 23 | } 24 | 25 | let fontSize = 12; 26 | if (fontSizeMatch) { 27 | supportedStyleAttrs.add('fontSize'); 28 | fontSize = parseFloat(fontSizeMatch[1]); 29 | } 30 | return fontSize; 31 | }; 32 | 33 | export default determineFontSize; 34 | -------------------------------------------------------------------------------- /docs/debugging/input-system-tester/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Input system testing | js-draw 7 | 13 | 14 | 15 | 16 |
17 | [ Real-time rendering | Fast rendering ] 18 |
19 |
20 |
21 |

Notes

22 |
    23 |
  • Above, orange strokes are the strokes added by js-draw.
  • 24 |
  • 25 | Above, white strokes more closely represent the actual input data received by 26 | js-draw. 27 |
  • 28 |
  • 29 | The image size reflects the size of orange strokes and metadata added by 30 | js-draw (excluding white strokes). 31 |
  • 32 |
  • 33 | Performance graphs are not averaged over multiple trials and are created using data from 34 | performance.now, which has limited accuracy. 35 |
  • 36 |
37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/math/src/rounding/toStringOfSamePrecision.test.ts: -------------------------------------------------------------------------------- 1 | import { toStringOfSamePrecision } from './toStringOfSamePrecision'; 2 | 3 | it('toStringOfSamePrecision', () => { 4 | expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23'); 5 | expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235'); 6 | expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2'); 7 | expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23'); 8 | expect(toStringOfSamePrecision(-1.23456, '1.1', '5.32')).toBe('-1.23'); 9 | expect(toStringOfSamePrecision(-1.99999, '1.1', '5.32')).toBe('-2'); 10 | expect(toStringOfSamePrecision(1.99999, '1.1', '5.32')).toBe('2'); 11 | expect(toStringOfSamePrecision(1.89999, '1.1', '5.32')).toBe('1.9'); 12 | expect(toStringOfSamePrecision(9.99999999, '-1.1234')).toBe('10'); 13 | expect(toStringOfSamePrecision(9.999999998999996, '100')).toBe('10'); 14 | expect(toStringOfSamePrecision(0.000012345, '0.000012')).toBe('.000012'); 15 | expect(toStringOfSamePrecision(0.000012645, '.000012')).toBe('.000013'); 16 | expect(toStringOfSamePrecision(-0.09999999999999432, '291.3')).toBe('-.1'); 17 | expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1'); 18 | expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9'); 19 | expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2'); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/InputFilter/InputMapper.ts: -------------------------------------------------------------------------------- 1 | import { InputEvt } from '../../inputEvents'; 2 | 3 | type OnEventCallback = (event: InputEvt) => boolean; 4 | 5 | export interface InputEventListener { 6 | // Returns true if handled 7 | onEvent: OnEventCallback; 8 | } 9 | 10 | /** 11 | * Accepts input events and emits input events. 12 | */ 13 | export default abstract class InputMapper implements InputEventListener { 14 | #listener: OnEventCallback | null = null; 15 | 16 | public constructor() {} 17 | 18 | // @internal 19 | public setEmitListener(listener: InputEventListener | OnEventCallback | null) { 20 | if (listener && typeof listener === 'object') { 21 | this.#listener = (event) => { 22 | return listener.onEvent(event) ?? false; 23 | }; 24 | } else { 25 | this.#listener = listener; 26 | } 27 | } 28 | 29 | protected emit(event: InputEvt): boolean { 30 | return this.#listener?.(event) ?? false; 31 | } 32 | 33 | /** 34 | * @returns true if the given `event` should be considered "handled" by the app and thus not 35 | * forwarded to other targets. For example, returning "true" for a touchpad pinch event prevents 36 | * the pinch event from zooming the webpage. 37 | * 38 | * Generally, this should return the result of calling `this.emit` with some event. 39 | */ 40 | public abstract onEvent(event: InputEvt): boolean; 41 | } 42 | -------------------------------------------------------------------------------- /docs/examples/example-multiple-editors/example.ts: -------------------------------------------------------------------------------- 1 | import * as jsdraw from 'js-draw'; 2 | import MaterialIconProvider from '@js-draw/material-icons'; 3 | import 'js-draw/styles'; 4 | 5 | const defaultSettings: Partial = { 6 | // Default to material icons 7 | iconProvider: new MaterialIconProvider(), 8 | 9 | // Only scroll the editor if it's focused. 10 | wheelEventsEnabled: 'only-if-focused', 11 | }; 12 | 13 | const editor1 = new jsdraw.Editor(document.body, defaultSettings); 14 | jsdraw.makeEdgeToolbar(editor1).addDefaults(); 15 | 16 | const editor2 = new jsdraw.Editor(document.body, defaultSettings); 17 | jsdraw.makeDropdownToolbar(editor2).addDefaults(); 18 | 19 | // Set up the "add editor" buttons 20 | const addEditorButton1: HTMLButtonElement = document.querySelector('button#add-editor-1')!; 21 | const addEditorButton2: HTMLButtonElement = document.querySelector('button#add-editor-2')!; 22 | 23 | addEditorButton1.onclick = () => { 24 | const editor = new jsdraw.Editor(document.body, defaultSettings); 25 | jsdraw.makeEdgeToolbar(editor).addDefaults(); 26 | }; 27 | 28 | addEditorButton2.onclick = () => { 29 | const editor = new jsdraw.Editor(document.body, { 30 | ...defaultSettings, 31 | 32 | // Use a different icon provider 33 | iconProvider: new jsdraw.IconProvider(), 34 | }); 35 | 36 | jsdraw.makeEdgeToolbar(editor).addDefaults(); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/js-draw/src/components/builders/types.ts: -------------------------------------------------------------------------------- 1 | import { Rect2 } from '@js-draw/math'; 2 | import AbstractRenderer from '../../rendering/renderers/AbstractRenderer'; 3 | import { StrokeDataPoint } from '../../types'; 4 | import Viewport from '../../Viewport'; 5 | import AbstractComponent from '../AbstractComponent'; 6 | import { StrokeStyle } from '../../rendering/RenderingStyle'; 7 | 8 | export interface ComponentBuilder { 9 | getBBox(): Rect2; 10 | build(): AbstractComponent; 11 | preview(renderer: AbstractRenderer): void; 12 | 13 | /** 14 | * (Optional) If provided, allows js-draw to efficiently render 15 | * an ink trail with the given style on some devices. 16 | */ 17 | inkTrailStyle?: () => StrokeStyle; 18 | 19 | /** 20 | * Called when the pen is stationary (or the user otherwise 21 | * activates autocomplete). This might attempt to fit the user's 22 | * drawing to a particular shape. 23 | * 24 | * The shape returned by this function may be ignored if it has 25 | * an empty bounding box. 26 | * 27 | * Although this returns a Promise, it should return *as fast as 28 | * possible*. 29 | */ 30 | autocorrectShape?: () => Promise; 31 | 32 | addPoint(point: StrokeDataPoint): void; 33 | } 34 | 35 | export type ComponentBuilderFactory = ( 36 | startPoint: StrokeDataPoint, 37 | viewport: Viewport, 38 | ) => ComponentBuilder; 39 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/ToolbarShortcutHandler.ts: -------------------------------------------------------------------------------- 1 | // Allows the toolbar to register keyboard events. 2 | // @packageDocumentation 3 | 4 | import Editor from '../Editor'; 5 | import { KeyPressEvent } from '../inputEvents'; 6 | import BaseTool from './BaseTool'; 7 | 8 | // Returns true if the event was handled, false otherwise. 9 | type KeyPressListener = (event: KeyPressEvent) => boolean; 10 | 11 | export default class ToolbarShortcutHandler extends BaseTool { 12 | private listeners: Set = new Set([]); 13 | public constructor(editor: Editor) { 14 | super(editor.notifier, editor.localization.changeTool); 15 | } 16 | 17 | public registerListener(listener: KeyPressListener) { 18 | this.listeners.add(listener); 19 | } 20 | 21 | public removeListener(listener: KeyPressListener) { 22 | this.listeners.delete(listener); 23 | } 24 | 25 | public override onKeyPress(event: KeyPressEvent): boolean { 26 | // TypeScript seems to automatically convert for of loops into for(init;check;update) 27 | // loops (even with target set to es6). Thus, we cannot iterate directly through the 28 | // set here. 29 | // See https://stackoverflow.com/q/48886500 30 | const listeners = Array.from(this.listeners.values()); 31 | for (const listener of listeners) { 32 | if (listener(event)) { 33 | return true; 34 | } 35 | } 36 | 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/dom/cloneElementWithStyles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes a clone of `element` and recursively applies styles from the original to the 3 | * clone's children. 4 | */ 5 | const cloneElementWithStyles = (element: HTMLElement) => { 6 | const restyle = (originalElement: HTMLElement, clonedElement: HTMLElement) => { 7 | const originalComputedStyle = getComputedStyle(originalElement); 8 | 9 | // jsdom doesn't support iterators in CSSStyleDeclarations. Iterate with 10 | // an index. 11 | for (let index = 0; index < originalComputedStyle.length; index++) { 12 | const propertyName = originalComputedStyle.item(index); 13 | 14 | const propertyValue = originalComputedStyle.getPropertyValue(propertyName); 15 | clonedElement.style?.setProperty(propertyName, propertyValue); 16 | } 17 | 18 | for (let i = 0; i < originalElement.children.length; i++) { 19 | const originalChild = originalElement.children.item(i) as HTMLElement; 20 | const clonedChild = clonedElement.children.item(i) as HTMLElement; 21 | 22 | if (originalChild && clonedChild) { 23 | restyle(originalChild, clonedChild); 24 | } else { 25 | console.warn('CloneElement: Missing child'); 26 | } 27 | } 28 | }; 29 | 30 | const elementClone = element.cloneNode(true) as HTMLElement; 31 | restyle(element, elementClone); 32 | return elementClone; 33 | }; 34 | 35 | export default cloneElementWithStyles; 36 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/components/makeGridSelector.scss: -------------------------------------------------------------------------------- 1 | .toolbar-grid-selector { 2 | position: relative; 3 | 4 | & > div { 5 | display: flex; 6 | flex-direction: row; 7 | 8 | max-width: 350px; 9 | flex-wrap: wrap; 10 | 11 | --button-size: 48px; 12 | } 13 | 14 | .choice-button { 15 | display: flex; 16 | flex-direction: column-reverse; 17 | box-sizing: border-box; 18 | cursor: pointer; 19 | 20 | flex-shrink: 1; 21 | margin: 2px; 22 | 23 | &.focus-visible { 24 | outline: 2px solid var(--foreground-color-1); 25 | } 26 | 27 | // The choice buttons use input[type=radio] internally. 28 | input { 29 | opacity: 0; 30 | height: 0; 31 | } 32 | 33 | label { 34 | display: flex; 35 | flex-direction: column; 36 | box-sizing: border-box; 37 | 38 | width: var(--button-size); 39 | height: var(--button-size); 40 | 41 | font-size: 0.7rem; 42 | align-items: center; 43 | justify-content: center; 44 | padding: 4px; 45 | 46 | // Prevent long pressing from selecting the label 47 | user-select: none; 48 | -webkit-user-select: none; 49 | } 50 | 51 | .icon { 52 | flex-grow: 1; 53 | flex-shrink: 1; 54 | width: 100%; 55 | } 56 | 57 | &.checked { 58 | background-color: var(--selection-background-color); 59 | color: var(--selection-foreground-color); 60 | --icon-color: var(--selection-foreground-color); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/math/src/rounding/toRoundedString.test.ts: -------------------------------------------------------------------------------- 1 | import { toRoundedString } from './toRoundedString'; 2 | 3 | describe('toRoundedString', () => { 4 | it('should round up numbers endings similar to .999999999999999', () => { 5 | expect(toRoundedString(0.999999999)).toBe('1'); 6 | expect(toRoundedString(0.899999999)).toBe('.9'); 7 | expect(toRoundedString(9.999999999)).toBe('10'); 8 | expect(toRoundedString(-10.999999999)).toBe('-11'); 9 | }); 10 | 11 | it('should round up numbers similar to 10.999999998', () => { 12 | expect(toRoundedString(10.999999998)).toBe('11'); 13 | }); 14 | 15 | it('should round strings with multiple digits after the ending decimal points', () => { 16 | expect(toRoundedString(292.2 - 292.8)).toBe('-.6'); 17 | expect(toRoundedString(4.06425600000023)).toBe('4.064256'); 18 | }); 19 | 20 | it('should round down strings ending endings similar to .00000001', () => { 21 | expect(toRoundedString(10.00000001)).toBe('10'); 22 | expect(toRoundedString(-30.00000001)).toBe('-30'); 23 | expect(toRoundedString(-14.20000000000002)).toBe('-14.2'); 24 | }); 25 | 26 | it('should not round numbers insufficiently close to the next', () => { 27 | expect(toRoundedString(-10.9999)).toBe('-10.9999'); 28 | expect(toRoundedString(-10.0001)).toBe('-10.0001'); 29 | expect(toRoundedString(-10.123499)).toBe('-10.123499'); 30 | expect(toRoundedString(0.00123499)).toBe('.00123499'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/material-icons/src/lib.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * # `@js-draw/material-icons` 3 | * 4 | * Provides a material icon theme for `js-draw`. 5 | * 6 | * @example 7 | * ```ts,runnable 8 | * import { Editor, makeEdgeToolbar } from 'js-draw'; 9 | * import { MaterialIconProvider } from '@js-draw/material-icons'; 10 | * 11 | * // Apply js-draw CSS 12 | * import 'js-draw/styles'; 13 | * 14 | * const editor = new Editor(document.body, { 15 | * iconProvider: new MaterialIconProvider(), 16 | * }); 17 | * 18 | * // Ensure that there is enough room for the toolbar 19 | * editor.getRootElement().style.minHeight = '500px'; 20 | * 21 | * // Add a toolbar 22 | * const toolbar = makeEdgeToolbar(editor); 23 | * 24 | * // ...with the default elements 25 | * toolbar.addDefaults(); 26 | * ``` 27 | * 28 | * @see 29 | * {@link MaterialIconProvider} 30 | * 31 | * @packageDocumentation 32 | */ 33 | 34 | import { EraserMode, IconProvider, SelectionMode } from 'js-draw'; 35 | import makeMaterialIconProviderClass from './makeMaterialIconProvider'; 36 | 37 | /** 38 | * An {@link js-draw!IconProvider | IconProvider} that uses [material icons](https://github.com/google/material-design-icons). 39 | */ 40 | const MaterialIconProvider = makeMaterialIconProviderClass({ 41 | IconProvider, 42 | EraserMode, 43 | SelectionMode, 44 | }); 45 | 46 | export { MaterialIconProvider, makeMaterialIconProviderClass }; 47 | export default MaterialIconProvider; 48 | -------------------------------------------------------------------------------- /packages/js-draw/src/util/mitLicenseAttribution.ts: -------------------------------------------------------------------------------- 1 | const mitLicenseAttribution = (copyright: string) => { 2 | const removeSingleLineBreaks = (text: string) => text.replace(/([^\n])[\n]([^\n])/g, '$1 $2'); 3 | 4 | return removeSingleLineBreaks(` 5 | MIT License 6 | 7 | Copyright (c) ${copyright} 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE.`); 26 | }; 27 | 28 | export default mitLicenseAttribution; 29 | -------------------------------------------------------------------------------- /docs/demo/ui/FloatingActionButton.css: -------------------------------------------------------------------------------- 1 | .floating-action-button.toplevel { 2 | position: fixed; 3 | bottom: 10px; 4 | right: 10px; 5 | } 6 | 7 | .floating-action-button .icon-wrapper { 8 | border-radius: 20px; 9 | background-color: var(--icon-background); 10 | margin-bottom: 2px; 11 | 12 | box-shadow: var(--shadow-color) -3px 0px 4px; 13 | width: var(--button-width); 14 | height: var(--button-width); 15 | 16 | transition: background-color 0.2s ease; 17 | } 18 | 19 | .floating-action-button > button { 20 | background-color: transparent; 21 | border: none; 22 | color: black; 23 | 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | 28 | --icon-color: black; 29 | --shadow-color: rgba(0, 0, 0, 0.5); 30 | --button-width: 50px; 31 | --icon-background: rgba(255, 0, 0, 1); 32 | 33 | width: var(--button-width); 34 | max-width: 40vw; 35 | } 36 | 37 | .floating-action-button .icon-wrapper:hover { 38 | --icon-background: rgba(255, 100, 100, 1); 39 | } 40 | 41 | .floating-action-button > .title { 42 | text-align: center; 43 | } 44 | 45 | .floating-action-button .submenu-container { 46 | position: relative; 47 | } 48 | 49 | .floating-action-button > .submenu-container.collapsed { 50 | display: none; 51 | } 52 | 53 | @media (prefers-color-scheme: dark) { 54 | .floating-action-button > button { 55 | color: white; 56 | --icon-color: white; 57 | --shadow-color: rgba(200, 200, 200, 0.4); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/math/src/polynomial/solveQuadratic.test.ts: -------------------------------------------------------------------------------- 1 | import solveQuadratic from './solveQuadratic'; 2 | 3 | describe('solveQuadratic', () => { 4 | it('should solve linear equations', () => { 5 | expect(solveQuadratic(0, 1, 2)).toMatchObject([-2, -2]); 6 | expect(solveQuadratic(0, 0, 2)[0]).toBeNaN(); 7 | }); 8 | 9 | it('should return both solutions to quadratic equations', () => { 10 | type TestCase = [[number, number, number], [number, number]]; 11 | 12 | const testCases: TestCase[] = [ 13 | [ 14 | [1, 0, 0], 15 | [0, 0], 16 | ], 17 | [ 18 | [2, 0, 0], 19 | [0, 0], 20 | ], 21 | 22 | [ 23 | [1, 0, -1], 24 | [1, -1], 25 | ], 26 | [ 27 | [1, 0, -4], 28 | [2, -2], 29 | ], 30 | [ 31 | [1, 0, 4], 32 | [NaN, NaN], 33 | ], 34 | 35 | [ 36 | [1, 1, 0], 37 | [0, -1], 38 | ], 39 | [ 40 | [1, 2, 0], 41 | [0, -2], 42 | ], 43 | 44 | [ 45 | [1, 2, 1], 46 | [-1, -1], 47 | ], 48 | [ 49 | [-9, 2, 1 / 3], 50 | [1 / 3, -1 / 9], 51 | ], 52 | ]; 53 | 54 | for (const [testCase, solution] of testCases) { 55 | const foundSolutions = solveQuadratic(...testCase); 56 | for (let i = 0; i < 2; i++) { 57 | if (isNaN(solution[i]) && isNaN(foundSolutions[i])) { 58 | expect(foundSolutions[i]).toBeNaN(); 59 | } else { 60 | expect(foundSolutions[i]).toBeCloseTo(solution[i]); 61 | } 62 | } 63 | } 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/ExitActionWidget.ts: -------------------------------------------------------------------------------- 1 | import { KeyPressEvent } from '../../inputEvents'; 2 | import Editor from '../../Editor'; 3 | import { ToolbarLocalization } from '../localization'; 4 | import ActionButtonWidget from './ActionButtonWidget'; 5 | import { ToolbarWidgetTag } from './BaseWidget'; 6 | import { exitKeyboardShortcut } from './keybindings'; 7 | import { ActionButtonIcon } from '../types'; 8 | 9 | class ExitActionWidget extends ActionButtonWidget { 10 | public constructor( 11 | editor: Editor, 12 | localization: ToolbarLocalization, 13 | saveCallback: () => void, 14 | labelOverride: Partial = {}, 15 | ) { 16 | super( 17 | editor, 18 | 'exit-button', 19 | // Creates an icon 20 | () => { 21 | return labelOverride.icon ?? editor.icons.makeCloseIcon(); 22 | }, 23 | labelOverride.label ?? localization.exit, 24 | saveCallback, 25 | ); 26 | this.setTags([ToolbarWidgetTag.Exit]); 27 | } 28 | 29 | protected override shouldAutoDisableInReadOnlyEditor() { 30 | return false; 31 | } 32 | 33 | protected override onKeyPress(event: KeyPressEvent): boolean { 34 | if (this.editor.shortcuts.matchesShortcut(exitKeyboardShortcut, event)) { 35 | this.clickAction(); 36 | return true; 37 | } 38 | return super.onKeyPress(event); 39 | } 40 | 41 | public override mustBeInToplevelMenu(): boolean { 42 | return true; 43 | } 44 | } 45 | 46 | export default ExitActionWidget; 47 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/ScrollbarTool.scss: -------------------------------------------------------------------------------- 1 | .ScrollbarTool-overlay { 2 | width: 0; 3 | height: 0; 4 | overflow: visible; 5 | 6 | $visible-opacity: 0.2; 7 | opacity: $visible-opacity; 8 | pointer-events: none; 9 | 10 | --fade-out-animation: 1s ease 0s fade-out; 11 | 12 | @media (prefers-reduced-motion: reduce) { 13 | --fade-out-animation: none !important; 14 | } 15 | 16 | @keyframes fade-out { 17 | from { 18 | opacity: $visible-opacity; 19 | } 20 | to { 21 | opacity: 0; 22 | } 23 | } 24 | 25 | &:not(.just-updated) { 26 | animation: var(--fade-out-animation); 27 | opacity: 0; 28 | } 29 | 30 | --scrollbar-size: 3px; 31 | 32 | .vertical-scrollbar, 33 | .horizontal-scrollbar { 34 | width: var(--scrollbar-size); 35 | height: var(--scrollbar-size); 36 | 37 | min-width: var(--scrollbar-size); 38 | min-height: var(--scrollbar-size); 39 | 40 | background-color: var(--foreground-color-1); 41 | border-radius: var(--scrollbar-size); 42 | position: absolute; 43 | 44 | &.represents-no-scroll { 45 | animation: var(--fade-out-animation); 46 | opacity: 0; 47 | } 48 | } 49 | 50 | &:not(.scrollbar-left) { 51 | .vertical-scrollbar { 52 | margin-left: calc(var(--editor-current-display-width-px) - var(--scrollbar-size)); 53 | } 54 | } 55 | 56 | &:not(.scrollbar-top) { 57 | .horizontal-scrollbar { 58 | margin-top: calc(var(--editor-current-display-height-px) - var(--scrollbar-size)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docs/debugging/stroke-logging/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Debugging | js-draw examples 7 | 39 | 40 | 41 |
42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /packages/js-draw/src/Coloris.css: -------------------------------------------------------------------------------- 1 | /* Imports Coloris' CSS and makes additional changes to the color picker */ 2 | 3 | #clr-picker { 4 | --clr-slider-size: 30px; 5 | } 6 | 7 | /* Coloris: Try to avoid scrolling instead of updating the color input. */ 8 | #clr-picker #clr-color-area, 9 | #clr-picker .clr_hue { 10 | touch-action: none; 11 | } 12 | 13 | /* Increase space between inputs */ 14 | #clr-picker .clr-alpha { 15 | margin-top: 15px; 16 | margin-bottom: 15px; 17 | } 18 | 19 | /* Increase size of input thumb to make it easier to select colors. */ 20 | #clr-picker.clr-picker input[type='range']::-moz-range-thumb { 21 | width: var(--clr-slider-size); 22 | height: var(--clr-slider-size); 23 | } 24 | 25 | /* Also apply to Chrome/iOS */ 26 | #clr-picker.clr-picker input[type='range']::-webkit-slider-thumb { 27 | /* 28 | Note: This doesn't seem to take effect in iOS if it's combined with the 29 | ::-moz-range-thumb rule above 30 | */ 31 | width: var(--clr-slider-size); 32 | height: var(--clr-slider-size); 33 | } 34 | 35 | #clr-picker.clr-picker input[type='range']::-webkit-slider-runnable-track { 36 | height: var(--clr-slider-size); 37 | } 38 | 39 | #clr-picker.clr-picker input[type='range']::-moz-range-track { 40 | height: var(--clr-slider-size); 41 | } 42 | 43 | /* 44 | Debugging: Uncommenting this rule makes Coloris' sliders more 45 | visible. 46 | 47 | #clr-picker.clr-picker input[type="range"] { 48 | opacity: 0.5; 49 | -webkit-appearance: auto; 50 | appearance: auto; 51 | } 52 | */ 53 | -------------------------------------------------------------------------------- /dist-test/editor-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Editor from a bundle 7 | 18 | 19 | 20 |

21 | This file tests the bundled version of js-draw. Be sure to run 22 | yarn run build before opening this! 23 |

24 | 25 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /packages/js-draw/src/tools/SelectionTool/types.ts: -------------------------------------------------------------------------------- 1 | import type { Rect2, Point2 } from '@js-draw/math'; 2 | import Pointer from '../../Pointer'; 3 | 4 | export enum SelectionMode { 5 | Lasso = 'lasso', 6 | Rectangle = 'rect', 7 | } 8 | 9 | export enum ResizeMode { 10 | Both, 11 | HorizontalOnly, 12 | VerticalOnly, 13 | } 14 | 15 | export enum TransformMode { 16 | Snap, 17 | NoSnap, 18 | } 19 | 20 | /** 21 | * Represents a child of the selection that should move with the selection 22 | * and handle events. 23 | * 24 | * Although selection children should be `HTMLElement`s, the selection may be 25 | * hidden behind an invisible element. As such, these elements should handle 26 | * drag start/update/end events. 27 | */ 28 | export interface SelectionBoxChild { 29 | /** 30 | * Update the position of this child, based on the screen position of 31 | * the selection box. 32 | */ 33 | updatePosition(selectionScreenBBox: Rect2): void; 34 | 35 | /** @returns true iff `point` (in editor **canvas** coordinates) is in this child. */ 36 | containsPoint(point: Point2): boolean; 37 | 38 | /** Adds this component's HTMLElement to the given `container`. */ 39 | addTo(container: HTMLElement): void; 40 | 41 | /** Removes this from its parent container. */ 42 | remove(): void; 43 | 44 | // handleDragStart will only be called for points such that containsPoint 45 | // returns true. 46 | handleDragStart(pointer: Pointer): boolean; 47 | handleDragUpdate(pointer: Pointer): void; 48 | handleDragEnd(): void; 49 | } 50 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/SaveActionWidget.ts: -------------------------------------------------------------------------------- 1 | import { KeyPressEvent } from '../../inputEvents'; 2 | import Editor from '../../Editor'; 3 | import { ToolbarLocalization } from '../localization'; 4 | import ActionButtonWidget from './ActionButtonWidget'; 5 | import { ToolbarWidgetTag } from './BaseWidget'; 6 | import { saveKeyboardShortcut } from './keybindings'; 7 | import { ActionButtonIcon } from '../types'; 8 | 9 | class SaveActionWidget extends ActionButtonWidget { 10 | public constructor( 11 | editor: Editor, 12 | localization: ToolbarLocalization, 13 | saveCallback: () => void, 14 | labelOverride: Partial = {}, 15 | ) { 16 | super( 17 | editor, 18 | 'save-button', 19 | // Creates an icon 20 | () => { 21 | return labelOverride.icon ?? editor.icons.makeSaveIcon(); 22 | }, 23 | labelOverride.label ?? localization.save, 24 | saveCallback, 25 | ); 26 | this.setTags([ToolbarWidgetTag.Save]); 27 | } 28 | 29 | protected override shouldAutoDisableInReadOnlyEditor() { 30 | return false; 31 | } 32 | 33 | protected override onKeyPress(event: KeyPressEvent): boolean { 34 | if (this.editor.shortcuts.matchesShortcut(saveKeyboardShortcut, event)) { 35 | this.clickAction(); 36 | return true; 37 | } 38 | 39 | // Run any default actions registered by the parent class. 40 | return super.onKeyPress(event); 41 | } 42 | 43 | public override mustBeInToplevelMenu(): boolean { 44 | return true; 45 | } 46 | } 47 | 48 | export default SaveActionWidget; 49 | -------------------------------------------------------------------------------- /packages/js-draw/src/UndoRedoHistory.test.ts: -------------------------------------------------------------------------------- 1 | import { Color4, EditorImage, Path, Stroke, Mat33, Vec2 } from './lib'; 2 | import { pathToRenderable } from './rendering/RenderablePathSpec'; 3 | import createEditor from './testing/createEditor'; 4 | 5 | describe('UndoRedoHistory', () => { 6 | it('should keep history size below maximum', () => { 7 | const editor = createEditor(); 8 | const stroke = new Stroke([ 9 | pathToRenderable(Path.fromString('m0,0 10,10'), { fill: Color4.red }), 10 | ]); 11 | editor.dispatch(EditorImage.addComponent(stroke)); 12 | 13 | for (let i = 0; i < editor.history['maxUndoRedoStackSize'] + 10; i++) { 14 | editor.dispatch(stroke.transformBy(Mat33.translation(Vec2.of(1, 1)))); 15 | } 16 | 17 | expect(editor.history.undoStackSize).toBeLessThan(editor.history['maxUndoRedoStackSize']); 18 | expect(editor.history.undoStackSize).toBeGreaterThan( 19 | editor.history['maxUndoRedoStackSize'] / 10, 20 | ); 21 | expect(editor.history.redoStackSize).toBe(0); 22 | 23 | const origUndoStackSize = editor.history.undoStackSize; 24 | while (editor.history.undoStackSize > 0) { 25 | editor.history.undo(); 26 | } 27 | 28 | // After undoing as much as possible, the stroke should still be present 29 | expect(editor.image.findParent(stroke)).not.toBe(null); 30 | 31 | // Undoing again shouldn't cause issues. 32 | editor.history.undo(); 33 | expect(editor.image.findParent(stroke)).not.toBe(null); 34 | 35 | expect(editor.history.redoStackSize).toBe(origUndoStackSize); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/js-draw/src/toolbar/widgets/components/makeSnappedList.scss: -------------------------------------------------------------------------------- 1 | // Repeat for specificity. 2 | // TODO(v2): Refactor everything to use RCSS. 3 | :root .toolbar-snapped-scroll-list.toolbar-snapped-scroll-list.toolbar-snapped-scroll-list { 4 | height: min(200px, 50vh); 5 | position: relative; 6 | display: flex; 7 | align-items: center; 8 | 9 | > .scroller { 10 | display: flex; 11 | flex-direction: column; 12 | overflow-y: auto; 13 | scroll-snap-type: y mandatory; 14 | 15 | height: 100%; 16 | width: 100%; 17 | flex-grow: 1; 18 | 19 | > .item { 20 | height: 100%; 21 | width: 100%; 22 | flex-shrink: 0; 23 | 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | 28 | scroll-snap-align: start; 29 | scroll-snap-stop: always; 30 | box-sizing: border-box; 31 | } 32 | } 33 | 34 | &.-empty { 35 | display: none; 36 | } 37 | 38 | > .page-markers { 39 | overflow: hidden; 40 | 41 | display: flex; 42 | flex-direction: column; 43 | align-items: center; 44 | 45 | max-height: 100%; 46 | min-height: 0; 47 | 48 | &.-one-element { 49 | visibility: hidden; 50 | } 51 | 52 | > .marker { 53 | > .content { 54 | background-color: var(--foreground-color-1); 55 | border-radius: 2px; 56 | padding: 2px; 57 | } 58 | 59 | padding: 2px; 60 | opacity: 0.1; 61 | cursor: pointer; 62 | 63 | left: 0; 64 | transition: left 0.2s ease; 65 | 66 | &.-active { 67 | position: relative; 68 | left: 2px; 69 | opacity: 0.2; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/js-draw/src/dialogs/makeAboutDialog.ts: -------------------------------------------------------------------------------- 1 | import type Editor from '../Editor'; 2 | import makeMessageDialog from './makeMessageDialog'; 3 | 4 | export interface AboutDialogLink { 5 | kind: 'link'; 6 | text: string; 7 | href: string; 8 | } 9 | 10 | export interface AboutDialogEntry { 11 | heading: string | AboutDialogLink; 12 | text?: string; 13 | minimized?: boolean; 14 | } 15 | 16 | const makeAboutDialog = (editor: Editor, entries: AboutDialogEntry[]) => { 17 | const dialog = makeMessageDialog(editor, { 18 | title: editor.localization.about, 19 | contentClassNames: ['about-dialog-content'], 20 | }); 21 | 22 | for (const entry of entries) { 23 | const container = document.createElement(entry.minimized ? 'details' : 'div'); 24 | container.classList.add('about-entry'); 25 | 26 | const header = document.createElement(entry.minimized ? 'summary' : 'h2'); 27 | 28 | if (typeof entry.heading === 'string') { 29 | header.innerText = entry.heading; 30 | } else { 31 | const link = document.createElement('a'); 32 | link.href = entry.heading.href.replace(/^javascript:/i, ''); 33 | link.text = entry.heading.text; 34 | header.appendChild(link); 35 | } 36 | 37 | container.appendChild(header); 38 | 39 | if (entry.text) { 40 | const bodyText = document.createElement('div'); 41 | bodyText.innerText = entry.text; 42 | 43 | container.appendChild(bodyText); 44 | } 45 | 46 | dialog.appendChild(container); 47 | } 48 | 49 | return { 50 | close: () => { 51 | return dialog.close(); 52 | }, 53 | }; 54 | }; 55 | 56 | export default makeAboutDialog; 57 | --------------------------------------------------------------------------------