├── .browserslistrc ├── public ├── CNAME ├── image.png ├── icon-42.png ├── book-solid.svg ├── code-solid.svg ├── sliders-h-solid.svg ├── hand-point-up-regular.svg └── index.html ├── babel.config.js ├── postcss.config.js ├── src ├── shims-vue.d.ts ├── assets │ └── logo.png ├── main.ts ├── shims-tsx.d.ts ├── App.vue ├── components │ ├── SvgTarget.vue │ ├── SvgArrow.vue │ ├── MatrixCode.vue │ ├── SvgGrid.vue │ ├── MatrixTable.vue │ ├── SvgDragPoint.vue │ ├── GHeader.vue │ ├── SvgGraph.vue │ └── HomeFloatingPanel.vue ├── misc.ts ├── DragHandler.ts └── pages │ └── HomePage.vue ├── doc └── understanding-transform-matrix.gif ├── README.md ├── .editorconfig ├── .gitignore ├── tslint.json ├── package.json └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | understanding-transform-matrix.ginpei.info 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginpei/understanding-transform-matrix/HEAD/public/image.png -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /public/icon-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginpei/understanding-transform-matrix/HEAD/public/icon-42.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginpei/understanding-transform-matrix/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /doc/understanding-transform-matrix.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginpei/understanding-transform-matrix/HEAD/doc/understanding-transform-matrix.gif -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | 4 | Vue.config.productionTip = false; 5 | 6 | new Vue({ 7 | render: (h) => h(App), 8 | }).$mount('#app'); 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Understanding `transform: matrix()` 2 | 3 | - http://understanding-transform-matrix.ginpei.info/ 4 | 5 | ![GIF: transform image by dragging and apply to CSS code](doc/understanding-transform-matrix.gif) 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "quotemark": [true, "single"], 13 | "indent": [true, "spaces", 2], 14 | "interface-name": false, 15 | "ordered-imports": false, 16 | "object-literal-sort-keys": false, 17 | "no-consecutive-blank-lines": false, 18 | "variable-name": [ 19 | true, 20 | "ban-keywords", 21 | "check-format", 22 | "allow-leading-underscore" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/book-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /public/code-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/SvgTarget.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transform-matrix", 3 | "version": "1.1.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint", 10 | "release": "npm run lint -s && npm run build -s && push-dir --dir dist --branch gh-pages" 11 | }, 12 | "dependencies": { 13 | "vue": "^2.5.17", 14 | "vue-class-component": "^6.0.0", 15 | "vue-property-decorator": "^7.0.0" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^3.1.1", 19 | "@vue/cli-plugin-typescript": "^3.1.1", 20 | "@vue/cli-service": "^3.1.1", 21 | "push-dir": "^0.4.1", 22 | "typescript": "^3.0.0", 23 | "vue-template-compiler": "^2.5.17" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/sliders-h-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/components/SvgArrow.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 50 | -------------------------------------------------------------------------------- /public/hand-point-up-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/misc.ts: -------------------------------------------------------------------------------- 1 | export const noop = () => undefined; 2 | 3 | export enum colors { 4 | i = 'red', 5 | j = 'blue', 6 | translation = 'green', 7 | } 8 | 9 | export interface IPos { 10 | x: number; 11 | y: number; 12 | } 13 | 14 | export const invalidPos: IPos = Object.freeze({ x: NaN, y: NaN }); 15 | export const zeroPos: IPos = Object.freeze({ x: 0, y: 0 }); 16 | 17 | export const getEventPositions = (event: MouseEvent | TouchEvent) => { 18 | let positions: IPos[]; 19 | 20 | if (event instanceof MouseEvent) { 21 | positions = [{ x: event.pageX, y: event.pageY }]; 22 | } else { 23 | positions = [...event.touches].map((touch) => 24 | ({ x: touch.pageX, y: touch.pageY })); 25 | } 26 | 27 | return positions; 28 | }; 29 | 30 | export interface IMatrix { 31 | ix: number; 32 | iy: number; 33 | jx: number; 34 | jy: number; 35 | tx: number; 36 | ty: number; 37 | } 38 | 39 | export const mergeMatrix = (m: IMatrix, diff: IMatrix): IMatrix => { 40 | return { 41 | ix: m.ix + diff.ix, 42 | iy: m.iy + diff.iy, 43 | jx: m.jx + diff.jx, 44 | jy: m.jy + diff.jy, 45 | tx: m.tx + diff.tx, 46 | ty: m.ty + diff.ty, 47 | }; 48 | }; 49 | export const roundMatrix = (m: IMatrix): IMatrix => { 50 | return { 51 | ix: fixMatrixNumber(m.ix), 52 | iy: fixMatrixNumber(m.iy), 53 | jx: fixMatrixNumber(m.jx), 54 | jy: fixMatrixNumber(m.jy), 55 | tx: fixMatrixNumber(m.tx), 56 | ty: fixMatrixNumber(m.ty), 57 | }; 58 | }; 59 | export const getMatrixStr = (m: IMatrix) => { 60 | const rm = roundMatrix(m); 61 | return `matrix(${[ 62 | rm.ix, rm.iy, 63 | rm.jx, rm.jy, 64 | rm.tx, rm.ty, 65 | ].join(', ')})`; 66 | }; 67 | 68 | export const fixMatrixNumber = (n: number) => { 69 | return Number(n.toFixed(2)); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/MatrixCode.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 46 | 47 | 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Understanding transform:matrix() 29 | 30 | 31 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/SvgGrid.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 70 | 71 | 80 | -------------------------------------------------------------------------------- /src/components/MatrixTable.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 52 | 53 | 72 | -------------------------------------------------------------------------------- /src/components/SvgDragPoint.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 71 | 72 | 105 | -------------------------------------------------------------------------------- /src/components/GHeader.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 44 | 45 | 126 | -------------------------------------------------------------------------------- /src/DragHandler.ts: -------------------------------------------------------------------------------- 1 | import { getEventPositions, IPos, invalidPos } from './misc'; 2 | 3 | export default class DragHandler { 4 | protected _dragging = false; 5 | protected el: HTMLElement | undefined = undefined; 6 | protected startedPos = invalidPos; 7 | protected lastPos = invalidPos; 8 | protected onEnd: ((diff: IPos) => void) | undefined = undefined; 9 | protected onMove: ((diff: IPos) => void) | undefined = undefined; 10 | protected onStart: ((data: { event: MouseEvent | TouchEvent, stop: () => void }) => void) | undefined = undefined; 11 | 12 | public get dragging() { 13 | return this._dragging; 14 | } 15 | 16 | constructor() { 17 | this.onMouseDown = this.onMouseDown.bind(this); 18 | this.onMouseMove = this.onMouseMove.bind(this); 19 | this.onMouseUp = this.onMouseUp.bind(this); 20 | this.onTouchStart = this.onTouchStart.bind(this); 21 | this.onTouchMove = this.onTouchMove.bind(this); 22 | this.onTouchEnd = this.onTouchEnd.bind(this); 23 | } 24 | 25 | public start(options: { 26 | el: HTMLElement, 27 | onEnd?: (diff: IPos) => void, 28 | onMove?: (diff: IPos) => void, 29 | onStart?: (data: { event: MouseEvent | TouchEvent, stop: () => void }) => void, 30 | }) { 31 | this.el = options.el; 32 | this.onEnd = options.onEnd; 33 | this.onMove = options.onMove; 34 | this.onStart = options.onStart; 35 | 36 | this.el.addEventListener('mousedown', this.onMouseDown); 37 | document.addEventListener('mousemove', this.onMouseMove); 38 | document.addEventListener('mouseup', this.onMouseUp); 39 | this.el.addEventListener('touchstart', this.onTouchStart); 40 | document.addEventListener('touchmove', this.onTouchMove); 41 | document.addEventListener('touchend', this.onTouchEnd); 42 | document.addEventListener('touchcancel', this.onTouchEnd); 43 | } 44 | 45 | public stop() { 46 | if (!this.el) { 47 | throw new Error('Unexpected situation: this.el must be set'); 48 | } 49 | 50 | this.el.removeEventListener('mousedown', this.onMouseDown); 51 | document.removeEventListener('mousemove', this.onMouseMove); 52 | document.removeEventListener('mouseup', this.onMouseUp); 53 | this.el.removeEventListener('touchstart', this.onTouchStart); 54 | document.removeEventListener('touchmove', this.onTouchMove); 55 | document.removeEventListener('touchend', this.onTouchEnd); 56 | document.removeEventListener('touchcancel', this.onTouchEnd); 57 | } 58 | 59 | public onMouseDown(event: MouseEvent) { 60 | event.preventDefault(); 61 | 62 | const [pos] = getEventPositions(event); 63 | this.startDragging(event, pos); 64 | } 65 | 66 | public onMouseMove = (event: MouseEvent) => { 67 | if (this._dragging) { 68 | const [pos] = getEventPositions(event); 69 | this.drag(pos); 70 | } 71 | } 72 | 73 | public onMouseUp = (event: MouseEvent) => { 74 | if (this._dragging) { 75 | const [pos] = getEventPositions(event); 76 | this.endDragging(pos); 77 | } 78 | } 79 | 80 | public onTouchStart(event: TouchEvent) { 81 | if (!this._dragging) { 82 | event.preventDefault(); 83 | 84 | const [pos] = getEventPositions(event); 85 | this.startDragging(event, pos); 86 | } 87 | } 88 | 89 | public onTouchMove = (event: TouchEvent) => { 90 | if (this._dragging) { 91 | const [pos] = getEventPositions(event); 92 | this.drag(pos); 93 | } 94 | } 95 | 96 | public onTouchEnd = (event: TouchEvent) => { 97 | if (this._dragging) { 98 | this.endDragging(this.lastPos); 99 | } 100 | } 101 | 102 | protected startDragging(event: MouseEvent | TouchEvent, pos: IPos) { 103 | let stopped = false; 104 | const stop = () => stopped = true; 105 | if (this.onStart) { 106 | this.onStart({ event, stop }); 107 | } 108 | 109 | if (stopped) { 110 | return; 111 | } 112 | 113 | this._dragging = true; 114 | this.startedPos = pos; 115 | } 116 | 117 | protected drag(pos: IPos) { 118 | if (!this._dragging) { 119 | return; 120 | } 121 | 122 | const diff: IPos = { 123 | x: pos.x - this.startedPos.x, 124 | y: pos.y - this.startedPos.y, 125 | }; 126 | 127 | this.lastPos = pos; 128 | 129 | if (this.onMove) { 130 | this.onMove(diff); 131 | } 132 | } 133 | 134 | protected endDragging(pos: IPos) { 135 | if (!this._dragging) { 136 | return; 137 | } 138 | 139 | const diff: IPos = { 140 | x: pos.x - this.startedPos.x, 141 | y: pos.y - this.startedPos.y, 142 | }; 143 | 144 | this.startedPos = invalidPos; 145 | this.lastPos = invalidPos; 146 | this._dragging = false; 147 | 148 | if (this.onEnd) { 149 | this.onEnd(diff); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/pages/HomePage.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 175 | 176 | 191 | -------------------------------------------------------------------------------- /src/components/SvgGraph.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 179 | 180 | 198 | -------------------------------------------------------------------------------- /src/components/HomeFloatingPanel.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 171 | 172 | 247 | --------------------------------------------------------------------------------