mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-05 23:43:47 +02:00
Merge pull request #11 from dscyrescotti/canvas
Implement canvas rendering
This commit is contained in:
@@ -12,6 +12,56 @@
|
||||
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
||||
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738792BE5EF0400A4542E /* MemosView.swift */; };
|
||||
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7387C2BE5EF4B00A4542E /* MemoView.swift */; };
|
||||
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738822BE5FEFE00A4542E /* RenderPass.swift */; };
|
||||
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738852BE5FF2500A4542E /* Canvas.swift */; };
|
||||
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738872BE5FF4400A4542E /* Renderer.swift */; };
|
||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738892BE6006A00A4542E /* PipelineStates.swift */; };
|
||||
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7388B2BE6009600A4542E /* Textures.swift */; };
|
||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA7388E2BE600DA00A4542E /* Grid.metal */; };
|
||||
ECA738912BE600F500A4542E /* Cache.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738902BE600F500A4542E /* Cache.metal */; };
|
||||
ECA738932BE6011100A4542E /* Stroke.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738922BE6011100A4542E /* Stroke.metal */; };
|
||||
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738942BE6012D00A4542E /* ViewPort.metal */; };
|
||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738962BE6014200A4542E /* Graphic.metal */; };
|
||||
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389B2BE601AF00A4542E /* GridVertex.swift */; };
|
||||
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389D2BE601CB00A4542E /* QuadVertex.swift */; };
|
||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */; };
|
||||
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A22BE6020A00A4542E /* CGFloat++.swift */; };
|
||||
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A52BE6023F00A4542E /* GridUniforms.swift */; };
|
||||
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A72BE6025900A4542E /* GraphicUniforms.swift */; };
|
||||
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A92BE6026D00A4542E /* Uniforms.swift */; };
|
||||
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738AC2BE60CC600A4542E /* DrawingView.swift */; };
|
||||
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */; };
|
||||
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B22BE60D9E00A4542E /* CanvasView.swift */; };
|
||||
ECA738B62BE60DCD00A4542E /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B52BE60DCD00A4542E /* History.swift */; };
|
||||
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */; };
|
||||
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B92BE60DEF00A4542E /* HistoryAction.swift */; };
|
||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738BB2BE60E0300A4542E /* Tool.swift */; };
|
||||
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738BE2BE60E3400A4542E /* Pen.swift */; };
|
||||
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C02BE60E5300A4542E /* PenStyle.swift */; };
|
||||
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */; };
|
||||
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */; };
|
||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C82BE60EF700A4542E /* GraphicContext.swift */; };
|
||||
ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */; };
|
||||
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738CC2BE60F2F00A4542E /* GridContext.swift */; };
|
||||
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D12BE60F7B00A4542E /* Stroke.swift */; };
|
||||
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */; };
|
||||
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */; };
|
||||
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */; };
|
||||
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */; };
|
||||
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */; };
|
||||
ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */; };
|
||||
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */; };
|
||||
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E32BE6110800A4542E /* Drawable.swift */; };
|
||||
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E52BE611FD00A4542E /* CGRect++.swift */; };
|
||||
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E72BE6120F00A4542E /* Color++.swift */; };
|
||||
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E92BE6122E00A4542E /* CGPoint++.swift */; };
|
||||
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */; };
|
||||
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */; };
|
||||
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738EF2BE6127700A4542E /* CGSize++.swift */; };
|
||||
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; };
|
||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
|
||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
||||
ECA738F82BE612EB00A4542E /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* Quad.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -21,6 +71,56 @@
|
||||
EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
ECA738792BE5EF0400A4542E /* MemosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosView.swift; sourceTree = "<group>"; };
|
||||
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoView.swift; sourceTree = "<group>"; };
|
||||
ECA738822BE5FEFE00A4542E /* RenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderPass.swift; sourceTree = "<group>"; };
|
||||
ECA738852BE5FF2500A4542E /* Canvas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Canvas.swift; sourceTree = "<group>"; };
|
||||
ECA738872BE5FF4400A4542E /* Renderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = "<group>"; };
|
||||
ECA738892BE6006A00A4542E /* PipelineStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipelineStates.swift; sourceTree = "<group>"; };
|
||||
ECA7388B2BE6009600A4542E /* Textures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Textures.swift; sourceTree = "<group>"; };
|
||||
ECA7388E2BE600DA00A4542E /* Grid.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Grid.metal; sourceTree = "<group>"; };
|
||||
ECA738902BE600F500A4542E /* Cache.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Cache.metal; sourceTree = "<group>"; };
|
||||
ECA738922BE6011100A4542E /* Stroke.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Stroke.metal; sourceTree = "<group>"; };
|
||||
ECA738942BE6012D00A4542E /* ViewPort.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ViewPort.metal; sourceTree = "<group>"; };
|
||||
ECA738962BE6014200A4542E /* Graphic.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Graphic.metal; sourceTree = "<group>"; };
|
||||
ECA7389B2BE601AF00A4542E /* GridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridVertex.swift; sourceTree = "<group>"; };
|
||||
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadVertex.swift; sourceTree = "<group>"; };
|
||||
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortVertex.swift; sourceTree = "<group>"; };
|
||||
ECA738A22BE6020A00A4542E /* CGFloat++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat++.swift"; sourceTree = "<group>"; };
|
||||
ECA738A52BE6023F00A4542E /* GridUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridUniforms.swift; sourceTree = "<group>"; };
|
||||
ECA738A72BE6025900A4542E /* GraphicUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicUniforms.swift; sourceTree = "<group>"; };
|
||||
ECA738A92BE6026D00A4542E /* Uniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uniforms.swift; sourceTree = "<group>"; };
|
||||
ECA738AC2BE60CC600A4542E /* DrawingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawingView.swift; sourceTree = "<group>"; };
|
||||
ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasViewController.swift; sourceTree = "<group>"; };
|
||||
ECA738B22BE60D9E00A4542E /* CanvasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasView.swift; sourceTree = "<group>"; };
|
||||
ECA738B52BE60DCD00A4542E /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
|
||||
ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEvent.swift; sourceTree = "<group>"; };
|
||||
ECA738B92BE60DEF00A4542E /* HistoryAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryAction.swift; sourceTree = "<group>"; };
|
||||
ECA738BB2BE60E0300A4542E /* Tool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tool.swift; sourceTree = "<group>"; };
|
||||
ECA738BE2BE60E3400A4542E /* Pen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pen.swift; sourceTree = "<group>"; };
|
||||
ECA738C02BE60E5300A4542E /* PenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenStyle.swift; sourceTree = "<group>"; };
|
||||
ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerPenStyle.swift; sourceTree = "<group>"; };
|
||||
ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserPenStyle.swift; sourceTree = "<group>"; };
|
||||
ECA738C82BE60EF700A4542E /* GraphicContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContext.swift; sourceTree = "<group>"; };
|
||||
ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortContext.swift; sourceTree = "<group>"; };
|
||||
ECA738CC2BE60F2F00A4542E /* GridContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridContext.swift; sourceTree = "<group>"; };
|
||||
ECA738D12BE60F7B00A4542E /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
|
||||
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeGenerator.swift; sourceTree = "<group>"; };
|
||||
ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidPointStrokeGenerator.swift; sourceTree = "<group>"; };
|
||||
ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheRenderPass.swift; sourceTree = "<group>"; };
|
||||
ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeRenderPass.swift; sourceTree = "<group>"; };
|
||||
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortRenderPass.swift; sourceTree = "<group>"; };
|
||||
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserRenderPass.swift; sourceTree = "<group>"; };
|
||||
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicRenderPass.swift; sourceTree = "<group>"; };
|
||||
ECA738E32BE6110800A4542E /* Drawable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawable.swift; sourceTree = "<group>"; };
|
||||
ECA738E52BE611FD00A4542E /* CGRect++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect++.swift"; sourceTree = "<group>"; };
|
||||
ECA738E72BE6120F00A4542E /* Color++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color++.swift"; sourceTree = "<group>"; };
|
||||
ECA738E92BE6122E00A4542E /* CGPoint++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint++.swift"; sourceTree = "<group>"; };
|
||||
ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGAffineTransform++.swift"; sourceTree = "<group>"; };
|
||||
ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "simd_float4x4++.swift"; sourceTree = "<group>"; };
|
||||
ECA738EF2BE6127700A4542E /* CGSize++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize++.swift"; sourceTree = "<group>"; };
|
||||
ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = "<group>"; };
|
||||
ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; };
|
||||
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
|
||||
ECA738F72BE612EB00A4542E /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -54,9 +154,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738762BE5EE4E00A4542E /* App */,
|
||||
ECA7387E2BE5FE4200A4542E /* Canvas */,
|
||||
ECA738A12BE601F700A4542E /* Extensions */,
|
||||
ECA738772BE5EEE800A4542E /* Features */,
|
||||
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */,
|
||||
EC7F6BF12BE5E6E400A34A7B /* Preview Content */,
|
||||
ECA738802BE5FE6000A4542E /* Resources */,
|
||||
);
|
||||
path = Memola;
|
||||
sourceTree = "<group>";
|
||||
@@ -102,6 +204,222 @@
|
||||
path = Memo;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738F92BE6130000A4542E /* Geometries */,
|
||||
ECA738812BE5FEEE00A4542E /* Abstracts */,
|
||||
ECA738992BE6018900A4542E /* Buffers */,
|
||||
ECA738C72BE60EE200A4542E /* Contexts */,
|
||||
ECA738842BE5FF1B00A4542E /* Core */,
|
||||
ECA738B42BE60DC200A4542E /* History */,
|
||||
ECA738D82BE60FE200A4542E /* RenderPasses */,
|
||||
ECA7388D2BE600BB00A4542E /* Shaders */,
|
||||
ECA738B12BE60D8800A4542E /* Tool */,
|
||||
ECA738AB2BE60CB500A4542E /* View */,
|
||||
);
|
||||
path = Canvas;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738802BE5FE6000A4542E /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738812BE5FEEE00A4542E /* Abstracts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738822BE5FEFE00A4542E /* RenderPass.swift */,
|
||||
ECA738E32BE6110800A4542E /* Drawable.swift */,
|
||||
);
|
||||
path = Abstracts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738842BE5FF1B00A4542E /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738852BE5FF2500A4542E /* Canvas.swift */,
|
||||
ECA738892BE6006A00A4542E /* PipelineStates.swift */,
|
||||
ECA738872BE5FF4400A4542E /* Renderer.swift */,
|
||||
ECA7388B2BE6009600A4542E /* Textures.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA7388D2BE600BB00A4542E /* Shaders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA7388E2BE600DA00A4542E /* Grid.metal */,
|
||||
ECA738902BE600F500A4542E /* Cache.metal */,
|
||||
ECA738922BE6011100A4542E /* Stroke.metal */,
|
||||
ECA738942BE6012D00A4542E /* ViewPort.metal */,
|
||||
ECA738962BE6014200A4542E /* Graphic.metal */,
|
||||
);
|
||||
path = Shaders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738982BE6015700A4542E /* Primitives */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738F72BE612EB00A4542E /* Quad.swift */,
|
||||
);
|
||||
path = Primitives;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738992BE6018900A4542E /* Buffers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738A42BE6022F00A4542E /* Uniforms */,
|
||||
ECA7389A2BE6019700A4542E /* Vertices */,
|
||||
);
|
||||
path = Buffers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA7389A2BE6019700A4542E /* Vertices */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA7389B2BE601AF00A4542E /* GridVertex.swift */,
|
||||
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */,
|
||||
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */,
|
||||
);
|
||||
path = Vertices;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738A12BE601F700A4542E /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738F32BE612A000A4542E /* Array++.swift */,
|
||||
ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */,
|
||||
ECA738A22BE6020A00A4542E /* CGFloat++.swift */,
|
||||
ECA738E92BE6122E00A4542E /* CGPoint++.swift */,
|
||||
ECA738E52BE611FD00A4542E /* CGRect++.swift */,
|
||||
ECA738EF2BE6127700A4542E /* CGSize++.swift */,
|
||||
ECA738F12BE6128F00A4542E /* Collection++.swift */,
|
||||
ECA738E72BE6120F00A4542E /* Color++.swift */,
|
||||
ECA738F52BE612B700A4542E /* MTLDevice++.swift */,
|
||||
ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738A42BE6022F00A4542E /* Uniforms */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738A52BE6023F00A4542E /* GridUniforms.swift */,
|
||||
ECA738A72BE6025900A4542E /* GraphicUniforms.swift */,
|
||||
ECA738A92BE6026D00A4542E /* Uniforms.swift */,
|
||||
);
|
||||
path = Uniforms;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738AB2BE60CB500A4542E /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738AE2BE60CEC00A4542E /* Bridge */,
|
||||
ECA738B22BE60D9E00A4542E /* CanvasView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738AE2BE60CEC00A4542E /* Bridge */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738AC2BE60CC600A4542E /* DrawingView.swift */,
|
||||
ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */,
|
||||
);
|
||||
path = Bridge;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738B12BE60D8800A4542E /* Tool */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738C22BE60E7200A4542E /* PenStyles */,
|
||||
ECA738BD2BE60E2800A4542E /* Pen */,
|
||||
ECA738BB2BE60E0300A4542E /* Tool.swift */,
|
||||
);
|
||||
path = Tool;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738B42BE60DC200A4542E /* History */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738B52BE60DCD00A4542E /* History.swift */,
|
||||
ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */,
|
||||
ECA738B92BE60DEF00A4542E /* HistoryAction.swift */,
|
||||
);
|
||||
path = History;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738BD2BE60E2800A4542E /* Pen */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738BE2BE60E3400A4542E /* Pen.swift */,
|
||||
ECA738C02BE60E5300A4542E /* PenStyle.swift */,
|
||||
);
|
||||
path = Pen;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738C22BE60E7200A4542E /* PenStyles */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */,
|
||||
ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */,
|
||||
);
|
||||
path = PenStyles;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738C72BE60EE200A4542E /* Contexts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738C82BE60EF700A4542E /* GraphicContext.swift */,
|
||||
ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */,
|
||||
ECA738CC2BE60F2F00A4542E /* GridContext.swift */,
|
||||
);
|
||||
path = Contexts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738CE2BE60F5000A4542E /* Stroke */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738D52BE60FA200A4542E /* Generators */,
|
||||
ECA738D12BE60F7B00A4542E /* Stroke.swift */,
|
||||
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */,
|
||||
);
|
||||
path = Stroke;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738D52BE60FA200A4542E /* Generators */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */,
|
||||
);
|
||||
path = Generators;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738D82BE60FE200A4542E /* RenderPasses */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */,
|
||||
ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */,
|
||||
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */,
|
||||
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */,
|
||||
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
|
||||
);
|
||||
path = RenderPasses;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECA738F92BE6130000A4542E /* Geometries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738CE2BE60F5000A4542E /* Stroke */,
|
||||
ECA738982BE6015700A4542E /* Primitives */,
|
||||
);
|
||||
path = Geometries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -172,9 +490,59 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */,
|
||||
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
|
||||
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
|
||||
ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */,
|
||||
ECA738912BE600F500A4542E /* Cache.metal in Sources */,
|
||||
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
|
||||
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
|
||||
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
|
||||
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
|
||||
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
|
||||
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
|
||||
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
||||
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
|
||||
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
|
||||
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
|
||||
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
|
||||
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
|
||||
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
||||
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
|
||||
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
||||
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
||||
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
|
||||
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
|
||||
ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */,
|
||||
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */,
|
||||
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */,
|
||||
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
|
||||
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
||||
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
||||
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
|
||||
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
|
||||
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
|
||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
||||
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
|
||||
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
||||
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
||||
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */,
|
||||
ECA738932BE6011100A4542E /* Stroke.metal in Sources */,
|
||||
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
|
||||
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
|
||||
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */,
|
||||
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */,
|
||||
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
|
||||
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
|
||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||
ECA738F82BE612EB00A4542E /* Quad.swift in Sources */,
|
||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
14
Memola/Canvas/Abstracts/Drawable.swift
Normal file
14
Memola/Canvas/Abstracts/Drawable.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Drawable.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
protocol Drawable: AnyObject {
|
||||
func prepare(device: MTLDevice)
|
||||
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder)
|
||||
}
|
||||
16
Memola/Canvas/Abstracts/RenderPass.swift
Normal file
16
Memola/Canvas/Abstracts/RenderPass.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// RenderPass.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
protocol RenderPass {
|
||||
var label: String { get }
|
||||
var descriptor: MTLRenderPassDescriptor? { get set }
|
||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer)
|
||||
func draw(on canvas: Canvas, with renderer: Renderer)
|
||||
}
|
||||
17
Memola/Canvas/Buffers/Uniforms/GraphicUniforms.swift
Normal file
17
Memola/Canvas/Buffers/Uniforms/GraphicUniforms.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// GraphicUniforms.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
struct GraphicUniforms {
|
||||
var color: vector_float4
|
||||
|
||||
init(color: [CGFloat]) {
|
||||
self.color = [color[0].float, color[1].float, color[2].float, color[3].float]
|
||||
}
|
||||
}
|
||||
15
Memola/Canvas/Buffers/Uniforms/GridUniforms.swift
Normal file
15
Memola/Canvas/Buffers/Uniforms/GridUniforms.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// GridUniforms.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
struct GridUniforms {
|
||||
var ratio: Float
|
||||
var zoom: Float
|
||||
var transform: simd_float4x4
|
||||
}
|
||||
13
Memola/Canvas/Buffers/Uniforms/Uniforms.swift
Normal file
13
Memola/Canvas/Buffers/Uniforms/Uniforms.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Uniforms.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
struct Uniforms {
|
||||
var transform: simd_float4x4
|
||||
}
|
||||
20
Memola/Canvas/Buffers/Vertices/GridVertex.swift
Normal file
20
Memola/Canvas/Buffers/Vertices/GridVertex.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// GridVertex.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
struct GridVertex {
|
||||
var position: vector_float4
|
||||
var pointSize: Float = 10
|
||||
}
|
||||
|
||||
extension GridVertex {
|
||||
init(x: CGFloat, y: CGFloat) {
|
||||
self.position = [x.float, y.float, 0, 1]
|
||||
}
|
||||
}
|
||||
32
Memola/Canvas/Buffers/Vertices/QuadVertex.swift
Normal file
32
Memola/Canvas/Buffers/Vertices/QuadVertex.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// QuadVertex.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
struct QuadVertex: Codable {
|
||||
var position: vector_float4
|
||||
var textCoord: vector_float2
|
||||
var color: vector_float4
|
||||
var origin: vector_float2
|
||||
var rotation: Float
|
||||
}
|
||||
|
||||
extension QuadVertex {
|
||||
init(x: CGFloat, y: CGFloat, textCoord: CGPoint, color: [CGFloat], origin: CGPoint, rotation: CGFloat) {
|
||||
self.position = [x.float, y.float, 0, 1]
|
||||
self.textCoord = [textCoord.x.float, textCoord.y.float]
|
||||
self.color = [color[0].float, color[1].float, color[2].float, color[3].float]
|
||||
self.origin = [origin.x.float, origin.y.float]
|
||||
self.rotation = rotation.float
|
||||
}
|
||||
|
||||
func getOrigin() -> CGPoint {
|
||||
CGPoint(x: CGFloat(origin[0]), y: CGFloat(origin[1]))
|
||||
}
|
||||
}
|
||||
|
||||
21
Memola/Canvas/Buffers/Vertices/ViewPortVertex.swift
Normal file
21
Memola/Canvas/Buffers/Vertices/ViewPortVertex.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// ViewPortVertex.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
struct ViewPortVertex {
|
||||
var position: vector_float4
|
||||
var textCoord: vector_float2
|
||||
}
|
||||
|
||||
extension ViewPortVertex {
|
||||
init(x: CGFloat, y: CGFloat, textCoord: CGPoint) {
|
||||
self.position = [x.float, y.float, 0, 1]
|
||||
self.textCoord = [textCoord.x.float, textCoord.y.float]
|
||||
}
|
||||
}
|
||||
129
Memola/Canvas/Contexts/GraphicContext.swift
Normal file
129
Memola/Canvas/Contexts/GraphicContext.swift
Normal file
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// GraphicContext.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
protocol GraphicContextDelegate: AnyObject {
|
||||
var didUpdate: PassthroughSubject<Void, Never> { get set }
|
||||
}
|
||||
|
||||
class GraphicContext: Codable {
|
||||
var strokes: [Stroke] = []
|
||||
var currentStroke: Stroke?
|
||||
var previousStroke: Stroke?
|
||||
var currentPoint: CGPoint?
|
||||
|
||||
var renderType: RenderType = .finished
|
||||
|
||||
var vertices: [ViewPortVertex] = []
|
||||
var vertexCount: Int = 4
|
||||
var vertexBuffer: MTLBuffer?
|
||||
|
||||
weak var delegate: GraphicContextDelegate?
|
||||
|
||||
init() {
|
||||
setViewPortVertices()
|
||||
}
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case strokes
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.strokes = try container.decode([Stroke].self, forKey: .strokes)
|
||||
setViewPortVertices()
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.strokes, forKey: .strokes)
|
||||
}
|
||||
|
||||
func setViewPortVertices() {
|
||||
vertexBuffer = nil
|
||||
vertices = [
|
||||
ViewPortVertex(x: -1, y: -1, textCoord: CGPoint(x: 0, y: 1)),
|
||||
ViewPortVertex(x: -1, y: 1, textCoord: CGPoint(x: 0, y: 0)),
|
||||
ViewPortVertex(x: 1, y: -1, textCoord: CGPoint(x: 1, y: 1)),
|
||||
ViewPortVertex(x: 1, y: 1, textCoord: CGPoint(x: 1, y: 0)),
|
||||
]
|
||||
}
|
||||
|
||||
func undoGraphic() {
|
||||
guard !strokes.isEmpty else { return }
|
||||
strokes.removeLast()
|
||||
previousStroke = nil
|
||||
delegate?.didUpdate.send()
|
||||
}
|
||||
|
||||
func redoGraphic(for event: HistoryEvent) {
|
||||
switch event {
|
||||
case .stroke(let stroke):
|
||||
strokes.append(stroke)
|
||||
previousStroke = nil
|
||||
}
|
||||
delegate?.didUpdate.send()
|
||||
}
|
||||
}
|
||||
|
||||
extension GraphicContext: Drawable {
|
||||
func prepare(device: MTLDevice) {
|
||||
guard vertexBuffer == nil else {
|
||||
return
|
||||
}
|
||||
vertexCount = vertices.count
|
||||
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<ViewPortVertex>.stride, options: [])
|
||||
}
|
||||
|
||||
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
prepare(device: device)
|
||||
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertices.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension GraphicContext {
|
||||
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
|
||||
let stroke = Stroke(
|
||||
color: pen.color,
|
||||
style: pen.style,
|
||||
thickness: pen.thickness
|
||||
)
|
||||
strokes.append(stroke)
|
||||
currentStroke = stroke
|
||||
currentPoint = point
|
||||
currentStroke?.begin(at: point)
|
||||
return stroke
|
||||
}
|
||||
|
||||
func appendStroke(with point: CGPoint) {
|
||||
guard let currentStroke else { return }
|
||||
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.style.stepRate else { return }
|
||||
currentStroke.append(to: point)
|
||||
self.currentPoint = point
|
||||
}
|
||||
|
||||
func endStroke(at point: CGPoint) {
|
||||
guard currentPoint != nil else { return }
|
||||
currentStroke?.finish(at: point)
|
||||
previousStroke = currentStroke
|
||||
currentStroke = nil
|
||||
self.currentPoint = nil
|
||||
delegate?.didUpdate.send()
|
||||
}
|
||||
}
|
||||
|
||||
extension GraphicContext {
|
||||
enum RenderType {
|
||||
case inProgress
|
||||
case newlyFinished
|
||||
case finished
|
||||
}
|
||||
}
|
||||
45
Memola/Canvas/Contexts/GridContext.swift
Normal file
45
Memola/Canvas/Contexts/GridContext.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// GridContext.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class GridContext {
|
||||
var vertices: [GridVertex] = []
|
||||
var vertexCount: Int = 0
|
||||
var vertexBuffer: MTLBuffer?
|
||||
|
||||
init() {
|
||||
generateVertices()
|
||||
}
|
||||
|
||||
func generateVertices() {
|
||||
let steps = stride(from: -10, through: 110, by: 0.25)
|
||||
for y in steps {
|
||||
for x in steps {
|
||||
vertices.append(GridVertex(x: CGFloat(x), y: CGFloat(y)))
|
||||
}
|
||||
}
|
||||
vertexCount = vertices.count
|
||||
}
|
||||
}
|
||||
|
||||
extension GridContext: Drawable {
|
||||
func prepare(device: MTLDevice) {
|
||||
guard vertexBuffer == nil else {
|
||||
return
|
||||
}
|
||||
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<GridVertex>.stride, options: [])
|
||||
}
|
||||
|
||||
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
guard vertexCount > .zero else { return }
|
||||
prepare(device: device)
|
||||
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||
renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: vertexCount)
|
||||
}
|
||||
}
|
||||
50
Memola/Canvas/Contexts/ViewPortContext.swift
Normal file
50
Memola/Canvas/Contexts/ViewPortContext.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// ViewPortContext.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class ViewPortContext {
|
||||
var vertices: [ViewPortVertex] = []
|
||||
let vertexCount: Int = 4
|
||||
var vertexBuffer: MTLBuffer?
|
||||
|
||||
func setViewPortVertices() {
|
||||
vertexBuffer = nil
|
||||
vertices = [
|
||||
ViewPortVertex(x: -1, y: -1, textCoord: CGPoint(x: 0, y: 1)),
|
||||
ViewPortVertex(x: -1, y: 1, textCoord: CGPoint(x: 0, y: 0)),
|
||||
ViewPortVertex(x: 1, y: -1, textCoord: CGPoint(x: 1, y: 1)),
|
||||
ViewPortVertex(x: 1, y: 1, textCoord: CGPoint(x: 1, y: 0)),
|
||||
]
|
||||
}
|
||||
|
||||
func setViewPortUpdateVertices(from bounds: CGRect) {
|
||||
vertexBuffer = nil
|
||||
vertices = [
|
||||
ViewPortVertex(x: bounds.minX, y: bounds.minY, textCoord: CGPoint(x: 0, y: 0)),
|
||||
ViewPortVertex(x: bounds.minX, y: bounds.maxY, textCoord: CGPoint(x: 0, y: 1)),
|
||||
ViewPortVertex(x: bounds.maxX, y: bounds.minY, textCoord: CGPoint(x: 1, y: 0)),
|
||||
ViewPortVertex(x: bounds.maxX, y: bounds.maxY, textCoord: CGPoint(x: 1, y: 1)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewPortContext: Drawable {
|
||||
func prepare(device: MTLDevice) {
|
||||
guard vertexBuffer == nil else {
|
||||
return
|
||||
}
|
||||
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<ViewPortVertex>.stride, options: [])
|
||||
}
|
||||
|
||||
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
prepare(device: device)
|
||||
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertices.count)
|
||||
}
|
||||
}
|
||||
191
Memola/Canvas/Core/Canvas.swift
Normal file
191
Memola/Canvas/Core/Canvas.swift
Normal file
@@ -0,0 +1,191 @@
|
||||
//
|
||||
// Canvas.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicContextDelegate {
|
||||
let size: CGSize
|
||||
let maximumZoomScale: CGFloat = 30
|
||||
let minimumZoomScale: CGFloat = 3.1
|
||||
|
||||
var transform: simd_float4x4 = .init()
|
||||
|
||||
var uniformsBuffer: MTLBuffer?
|
||||
|
||||
let gridContext = GridContext()
|
||||
var graphicContext = GraphicContext()
|
||||
let viewPortContext = ViewPortContext()
|
||||
|
||||
var clipBounds: CGRect = .zero
|
||||
var zoomScale: CGFloat = .zero
|
||||
|
||||
// weak var board: BoardObject?
|
||||
var graphicLoader: (() throws -> GraphicContext)?
|
||||
|
||||
@Published var state: State = .initial
|
||||
lazy var didUpdate = PassthroughSubject<Void, Never>()
|
||||
|
||||
init(size: CGSize = .init(width: 8_000, height: 8_000)) {
|
||||
self.size = size
|
||||
}
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case size
|
||||
case graphicContext
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.size = try container.decode(CGSize.self, forKey: .size)
|
||||
self.graphicLoader = { try container.decode(GraphicContext.self, forKey: .graphicContext) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
extension Canvas {
|
||||
func load() {
|
||||
guard let graphicLoader else { return }
|
||||
Task(priority: .high) { [unowned self, graphicLoader] in
|
||||
await MainActor.run {
|
||||
self.state = .loading
|
||||
}
|
||||
do {
|
||||
let graphicContext = try graphicLoader()
|
||||
graphicContext.delegate = self
|
||||
await MainActor.run {
|
||||
self.graphicContext = graphicContext
|
||||
self.state = .loaded
|
||||
}
|
||||
} catch {
|
||||
NSLog("[SketchNote] - \(error.localizedDescription)")
|
||||
await MainActor.run {
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func save(on managedObjectContext: NSManagedObjectContext) async {
|
||||
// guard let board else { return }
|
||||
// do {
|
||||
// board.data = try JSONEncoder().encode(self)
|
||||
// board.updatedAt = Date()
|
||||
// try managedObjectContext.save()
|
||||
// } catch {
|
||||
// NSLog("[SketchNote] - \(error.localizedDescription)")
|
||||
// }
|
||||
}
|
||||
|
||||
func listen(on managedObjectContext: NSManagedObjectContext) {
|
||||
Task(priority: .background) { [unowned self] in
|
||||
for await _ in didUpdate.values {
|
||||
await save(on: managedObjectContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Dimension
|
||||
extension Canvas {
|
||||
func updateTransform(on drawingView: DrawingView) {
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
let renderView = drawingView.renderView
|
||||
let targetRect = drawingView.convert(drawingView.bounds, to: renderView)
|
||||
let transform1 = bounds.transform(to: targetRect)
|
||||
let transform2 = renderView.bounds.transform(to: CGRect(x: -1.0, y: -1.0, width: 2.0, height: 2.0))
|
||||
let transform3 = CGAffineTransform.identity.translatedBy(x: 0, y: 1).scaledBy(x: 1, y: -1).translatedBy(x: 0, y: 1)
|
||||
self.transform = simd_float4x4(transform1 * transform2 * transform3)
|
||||
}
|
||||
|
||||
func updateClipBounds(_ scrollView: UIScrollView, on drawingView: DrawingView) {
|
||||
let ratio = drawingView.ratio
|
||||
let bounds = scrollView.convert(scrollView.bounds, to: drawingView)
|
||||
clipBounds = CGRect(origin: bounds.origin.muliply(by: ratio), size: bounds.size.multiply(by: ratio))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Zoom Scale
|
||||
extension Canvas {
|
||||
func setZoomScale(_ zoomScale: CGFloat) {
|
||||
self.zoomScale = zoomScale
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Graphic Context
|
||||
extension Canvas {
|
||||
func beginTouch(at point: CGPoint, pen: Pen) -> Stroke {
|
||||
graphicContext.beginStroke(at: point, pen: pen)
|
||||
}
|
||||
|
||||
func moveTouch(to point: CGPoint) {
|
||||
graphicContext.appendStroke(with: point)
|
||||
}
|
||||
|
||||
func endTouch(at point: CGPoint) {
|
||||
graphicContext.endStroke(at: point)
|
||||
}
|
||||
|
||||
func setGraphicRenderType(_ renderType: GraphicContext.RenderType) {
|
||||
graphicContext.renderType = renderType
|
||||
}
|
||||
|
||||
func getNewlyAddedStroke() -> Stroke? {
|
||||
graphicContext.strokes.last
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Rendering
|
||||
extension Canvas {
|
||||
func renderGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
var uniforms = GridUniforms(
|
||||
ratio: size.width.float / 100,
|
||||
zoom: zoomScale.float,
|
||||
transform: transform
|
||||
)
|
||||
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<GridUniforms>.size)
|
||||
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||
gridContext.draw(device: device, renderEncoder: renderEncoder)
|
||||
}
|
||||
|
||||
func renderGraphic(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
graphicContext.draw(device: device, renderEncoder: renderEncoder)
|
||||
}
|
||||
|
||||
func renderViewPort(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
viewPortContext.setViewPortVertices()
|
||||
viewPortContext.draw(device: device, renderEncoder: renderEncoder)
|
||||
}
|
||||
|
||||
func renderViewPortUpdate(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
var uniforms = Uniforms(transform: transform)
|
||||
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
||||
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||
viewPortContext.setViewPortUpdateVertices(from: clipBounds)
|
||||
viewPortContext.draw(device: device, renderEncoder: renderEncoder)
|
||||
}
|
||||
|
||||
func setUniformsBuffer(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
var uniforms = Uniforms(transform: transform)
|
||||
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
||||
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
extension Canvas {
|
||||
enum State {
|
||||
case initial
|
||||
case loading
|
||||
case loaded
|
||||
case closing
|
||||
case closed
|
||||
case failed
|
||||
}
|
||||
}
|
||||
134
Memola/Canvas/Core/PipelineStates.swift
Normal file
134
Memola/Canvas/Core/PipelineStates.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// PipelineStates.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
struct PipelineStates {
|
||||
static func createGridPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||
let device = renderer.device
|
||||
let library = renderer.library
|
||||
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_grid")
|
||||
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_grid")
|
||||
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||
pipelineDescriptor.label = "Grid Pipeline State"
|
||||
|
||||
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||
attachment?.isBlendingEnabled = true
|
||||
attachment?.rgbBlendOperation = .add
|
||||
attachment?.sourceRGBBlendFactor = .one
|
||||
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
attachment?.alphaBlendOperation = .add
|
||||
attachment?.sourceAlphaBlendFactor = .sourceAlpha
|
||||
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||
}
|
||||
|
||||
static func createGraphicPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||
let device = renderer.device
|
||||
let library = renderer.library
|
||||
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_graphic")
|
||||
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_graphic")
|
||||
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||
pipelineDescriptor.label = "Graphic Pipeline State"
|
||||
|
||||
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||
attachment?.isBlendingEnabled = true
|
||||
attachment?.rgbBlendOperation = .add
|
||||
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
attachment?.alphaBlendOperation = .add
|
||||
attachment?.sourceAlphaBlendFactor = .one
|
||||
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||
}
|
||||
|
||||
static func createStrokePipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||
let device = renderer.device
|
||||
let library = renderer.library
|
||||
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_stroke")
|
||||
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_stroke")
|
||||
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||
pipelineDescriptor.label = "Stroke Pipeline State"
|
||||
|
||||
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||
attachment?.isBlendingEnabled = true
|
||||
attachment?.rgbBlendOperation = .add
|
||||
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
attachment?.alphaBlendOperation = .add
|
||||
attachment?.sourceAlphaBlendFactor = .one
|
||||
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||
}
|
||||
|
||||
static func createEraserPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||
let device = renderer.device
|
||||
let library = renderer.library
|
||||
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_stroke")
|
||||
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_stroke")
|
||||
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||
pipelineDescriptor.label = "Eraser Pipeline State"
|
||||
|
||||
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||
attachment?.isBlendingEnabled = true
|
||||
attachment?.rgbBlendOperation = .add
|
||||
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||
attachment?.destinationRGBBlendFactor = .one
|
||||
attachment?.alphaBlendOperation = .reverseSubtract
|
||||
attachment?.sourceAlphaBlendFactor = .sourceAlpha
|
||||
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||
}
|
||||
|
||||
static func createViewPortPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil, isUpdate: Bool = false) -> MTLRenderPipelineState? {
|
||||
var label: String
|
||||
var vertexName: String
|
||||
if isUpdate {
|
||||
label = "View Port Update Pipeline State"
|
||||
vertexName = "vertex_viewport_update"
|
||||
} else {
|
||||
label = "View Port Pipeline State"
|
||||
vertexName = "vertex_viewport"
|
||||
}
|
||||
let device = renderer.device
|
||||
let library = renderer.library
|
||||
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||
pipelineDescriptor.vertexFunction = library.makeFunction(name: vertexName)
|
||||
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_viewport")
|
||||
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||
pipelineDescriptor.label = label
|
||||
|
||||
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||
attachment?.isBlendingEnabled = true
|
||||
attachment?.rgbBlendOperation = .add
|
||||
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
attachment?.alphaBlendOperation = .add
|
||||
attachment?.sourceAlphaBlendFactor = .one
|
||||
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||
}
|
||||
|
||||
static func createCachePipelineState(from renderer: Renderer) -> MTLComputePipelineState? {
|
||||
let device = renderer.device
|
||||
let library = renderer.library
|
||||
guard let function = library.makeFunction(name: "copy_texture_viewport") else {
|
||||
return nil
|
||||
}
|
||||
return try? device.makeComputePipelineState(function: function)
|
||||
}
|
||||
}
|
||||
85
Memola/Canvas/Core/Renderer.swift
Normal file
85
Memola/Canvas/Core/Renderer.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Renderer.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
final class Renderer {
|
||||
var device: MTLDevice
|
||||
var library: MTLLibrary
|
||||
var pixelFormat: MTLPixelFormat
|
||||
var commandQueue: MTLCommandQueue
|
||||
|
||||
var redrawsGraphicRender: Bool = true
|
||||
var updatesViewPort: Bool = false
|
||||
|
||||
var canvasView: MTKView
|
||||
|
||||
lazy var strokeRenderPass: StrokeRenderPass = {
|
||||
StrokeRenderPass(renderer: self)
|
||||
}()
|
||||
lazy var eraserRenderPass: EraserRenderPass = {
|
||||
EraserRenderPass(renderer: self)
|
||||
}()
|
||||
lazy var graphicRenderPass: GraphicRenderPass = {
|
||||
GraphicRenderPass(renderer: self)
|
||||
}()
|
||||
lazy var cacheRenderPass: CacheRenderPass = {
|
||||
CacheRenderPass(renderer: self)
|
||||
}()
|
||||
lazy var viewPortRenderPass: ViewPortRenderPass = {
|
||||
ViewPortRenderPass(renderer: self)
|
||||
}()
|
||||
|
||||
init(canvasView: MTKView) {
|
||||
guard let device = MTLCreateSystemDefaultDevice() else {
|
||||
fatalError("[Error]: Unable to create system default device.")
|
||||
}
|
||||
guard let commandQueue = device.makeCommandQueue() else {
|
||||
fatalError("[Error]: Unable to create command queue.")
|
||||
}
|
||||
guard let library = device.makeDefaultLibrary() else {
|
||||
fatalError("[Error]: Unable to create default library.")
|
||||
}
|
||||
self.device = device
|
||||
self.commandQueue = commandQueue
|
||||
self.library = library
|
||||
self.pixelFormat = canvasView.colorPixelFormat
|
||||
self.canvasView = canvasView
|
||||
canvasView.device = device
|
||||
self.viewPortRenderPass.view = canvasView
|
||||
}
|
||||
|
||||
func resize(on view: MTKView, to size: CGSize) {
|
||||
if !updatesViewPort {
|
||||
strokeRenderPass.resize(on: view, to: size, with: self)
|
||||
graphicRenderPass.resize(on: view, to: size, with: self)
|
||||
cacheRenderPass.resize(on: view, to: size, with: self)
|
||||
}
|
||||
viewPortRenderPass.resize(on: view, to: size, with: self)
|
||||
redrawsGraphicRender = true
|
||||
}
|
||||
|
||||
func draw(in view: MTKView, on canvas: Canvas) {
|
||||
if !updatesViewPort {
|
||||
graphicRenderPass.strokeRenderPass = strokeRenderPass
|
||||
graphicRenderPass.eraserRenderPass = eraserRenderPass
|
||||
graphicRenderPass.draw(on: canvas, with: self)
|
||||
}
|
||||
|
||||
cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture
|
||||
cacheRenderPass.strokeRenderPass = strokeRenderPass
|
||||
cacheRenderPass.eraserRenderPass = eraserRenderPass
|
||||
cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture
|
||||
cacheRenderPass.graphicPipelineState = graphicRenderPass.graphicPipelineState
|
||||
cacheRenderPass.draw(on: canvas, with: self)
|
||||
|
||||
viewPortRenderPass.descriptor = view.currentRenderPassDescriptor
|
||||
viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture
|
||||
viewPortRenderPass.draw(on: canvas, with: self)
|
||||
}
|
||||
}
|
||||
93
Memola/Canvas/Core/Textures.swift
Normal file
93
Memola/Canvas/Core/Textures.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Textures.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class Textures {
|
||||
static var penTextures: [String: MTLTexture] = [:]
|
||||
|
||||
@discardableResult
|
||||
static func createPenTexture(with textureName: String, on device: MTLDevice) -> MTLTexture? {
|
||||
if let penTexture = penTextures[textureName] {
|
||||
return penTexture
|
||||
}
|
||||
let textureLoader = MTKTextureLoader(device: device)
|
||||
let penTexture = try? textureLoader.newTexture(name: textureName, scaleFactor: 1.0, bundle: .main, options: [.SRGB: false])
|
||||
penTextures[textureName] = penTexture
|
||||
return penTexture
|
||||
}
|
||||
|
||||
static func createGraphicTexture(
|
||||
from renderer: Renderer,
|
||||
size: CGSize,
|
||||
pixelFormat: MTLPixelFormat? = nil
|
||||
) -> MTLTexture? {
|
||||
let width = Int(size.width)
|
||||
let height = Int(size.height)
|
||||
guard width > 0, height > 0 else { return nil }
|
||||
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||
pixelFormat: pixelFormat ?? renderer.pixelFormat,
|
||||
width: width,
|
||||
height: height,
|
||||
mipmapped: false
|
||||
)
|
||||
descriptor.storageMode = .shared
|
||||
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
|
||||
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
|
||||
return nil
|
||||
}
|
||||
texture.label = "Graphic Texture"
|
||||
return texture
|
||||
}
|
||||
|
||||
static func createCacheTexture(
|
||||
from renderer: Renderer,
|
||||
size: CGSize,
|
||||
pixelFormat: MTLPixelFormat? = nil
|
||||
) -> MTLTexture? {
|
||||
let width = Int(size.width)
|
||||
let height = Int(size.height)
|
||||
guard width > 0, height > 0 else { return nil }
|
||||
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||
pixelFormat: pixelFormat ?? renderer.pixelFormat,
|
||||
width: width,
|
||||
height: height,
|
||||
mipmapped: false
|
||||
)
|
||||
descriptor.storageMode = .shared
|
||||
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
|
||||
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
|
||||
return nil
|
||||
}
|
||||
texture.label = "Cache Texture"
|
||||
return texture
|
||||
}
|
||||
|
||||
static func createStrokeTexture(
|
||||
from renderer: Renderer,
|
||||
size: CGSize,
|
||||
pixelFormat: MTLPixelFormat? = nil
|
||||
) -> MTLTexture? {
|
||||
let width = Int(size.width)
|
||||
let height = Int(size.height)
|
||||
guard width > 0, height > 0 else { return nil }
|
||||
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||
pixelFormat: pixelFormat ?? renderer.pixelFormat,
|
||||
width: width,
|
||||
height: height,
|
||||
mipmapped: false
|
||||
)
|
||||
descriptor.storageMode = .shared
|
||||
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
|
||||
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
|
||||
return nil
|
||||
}
|
||||
texture.label = "Stroke Texture"
|
||||
return texture
|
||||
}
|
||||
}
|
||||
106
Memola/Canvas/Geometries/Primitives/Quad.swift
Normal file
106
Memola/Canvas/Geometries/Primitives/Quad.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// Quad.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class Quad {
|
||||
var origin: CGPoint
|
||||
var color: [CGFloat]
|
||||
var size: CGFloat
|
||||
var rotation: CGFloat
|
||||
var vertices: [QuadVertex] = []
|
||||
|
||||
var vertexBuffer: MTLBuffer?
|
||||
var vertexCount: Int = 0
|
||||
|
||||
init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: Shape = .rounded) {
|
||||
self.origin = origin
|
||||
self.size = size
|
||||
self.color = color
|
||||
self.rotation = rotation
|
||||
generateVertices(shape)
|
||||
}
|
||||
|
||||
func generateVertices(_ shape: Shape) {
|
||||
switch shape {
|
||||
case .rounded:
|
||||
generateRoundedQuad()
|
||||
case .squared:
|
||||
generateSquaredQuad()
|
||||
case let .calligraphic(vFactor, hFactor):
|
||||
generateCalligraphicQuad(vFactor: vFactor, hFactor: hFactor)
|
||||
case let .trapezoid(topFactor, bottomFactor, heightFactor):
|
||||
generateTrapezoidQuad(topFactor: topFactor, bottomFactor: bottomFactor, heightFactor: heightFactor)
|
||||
}
|
||||
}
|
||||
|
||||
func generateRoundedQuad() {
|
||||
let halfSize = size * 0.5
|
||||
vertices = [
|
||||
QuadVertex(x: origin.x - halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||
]
|
||||
vertexCount = vertices.count
|
||||
}
|
||||
|
||||
func generateSquaredQuad() {
|
||||
let vHalfSize = size * 0.5
|
||||
let hHalfSize = size * 0.15
|
||||
vertices = [
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||
]
|
||||
vertexCount = vertices.count
|
||||
}
|
||||
|
||||
func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) {
|
||||
let vHalfSize = size * vFactor * 0.5
|
||||
let hHalfSize = size * hFactor * 0.5
|
||||
vertices = [
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||
]
|
||||
vertexCount = vertices.count
|
||||
}
|
||||
|
||||
func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) {
|
||||
let vHalfSize = size * heightFactor * 0.5
|
||||
let hTopHalfSize = size * topFactor * 0.5
|
||||
let hBottomHalfSize = size * bottomFactor * 0.5
|
||||
vertices = [
|
||||
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||
]
|
||||
vertexCount = vertices.count
|
||||
}
|
||||
}
|
||||
|
||||
extension Quad {
|
||||
enum Shape {
|
||||
case rounded
|
||||
case squared
|
||||
case calligraphic(CGFloat, CGFloat)
|
||||
case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// SolidPointStrokeGenerator.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||
var configuration: Configuration
|
||||
|
||||
func begin(at point: CGPoint, on stroke: Stroke) {
|
||||
stroke.keyPoints.append(point)
|
||||
addPoint(point, on: stroke)
|
||||
}
|
||||
|
||||
func append(to point: CGPoint, on stroke: Stroke) {
|
||||
guard stroke.keyPoints.count > 0 else {
|
||||
return
|
||||
}
|
||||
stroke.keyPoints.append(point)
|
||||
switch stroke.keyPoints.count {
|
||||
case 2:
|
||||
let start = stroke.keyPoints[0]
|
||||
let end = stroke.keyPoints[1]
|
||||
let control = CGPoint.middle(p1: start, p2: end)
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
case 3:
|
||||
discardPoints(upto: stroke.vertexIndex, on: stroke)
|
||||
let index = stroke.keyPoints.count - 1
|
||||
var start = stroke.keyPoints[index - 2]
|
||||
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||
var control = CGPoint.middle(p1: start, p2: end)
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||
control = stroke.keyPoints[index - 1]
|
||||
end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
default:
|
||||
smoothOutPath(on: stroke)
|
||||
let index = stroke.keyPoints.count - 1
|
||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||
let control = stroke.keyPoints[index - 1]
|
||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
}
|
||||
}
|
||||
|
||||
func finish(at point: CGPoint, on stroke: Stroke) {
|
||||
switch stroke.keyPoints.count {
|
||||
case 0...1:
|
||||
break
|
||||
default:
|
||||
append(to: point, on: stroke)
|
||||
let index = stroke.keyPoints.count - 1
|
||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||
let end = stroke.keyPoints[index]
|
||||
let control = CGPoint.middle(p1: start, p2: end)
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
}
|
||||
}
|
||||
|
||||
private func smoothOutPath(on stroke: Stroke) {
|
||||
discardPoints(upto: stroke.vertexIndex, on: stroke)
|
||||
adjustPreviousKeyPoint(on: stroke)
|
||||
switch stroke.keyPoints.count {
|
||||
case 4:
|
||||
let index = stroke.keyPoints.count - 2
|
||||
let start = stroke.keyPoints[index - 2]
|
||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||
let control = CGPoint.middle(p1: start, p2: end)
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
fallthrough
|
||||
default:
|
||||
let index = stroke.keyPoints.count - 2
|
||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||
let control = stroke.keyPoints[index - 1]
|
||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
}
|
||||
stroke.vertexIndex = stroke.vertices.endIndex - 1
|
||||
}
|
||||
|
||||
private func adjustPreviousKeyPoint(on stroke: Stroke) {
|
||||
let index = stroke.keyPoints.count - 1
|
||||
let prev = stroke.keyPoints[index - 1]
|
||||
let current = stroke.keyPoints[index]
|
||||
let averageX = (prev.x + current.x) / 2
|
||||
let averageY = (prev.y + current.y) / 2
|
||||
let point = CGPoint(x: averageX, y: averageY)
|
||||
stroke.keyPoints[index] = point
|
||||
stroke.keyPoints[index - 1] = point
|
||||
}
|
||||
|
||||
private func addPoint(_ point: CGPoint, on stroke: Stroke) {
|
||||
let rotation: CGFloat
|
||||
switch configuration.rotation {
|
||||
case .fixed:
|
||||
rotation = 0
|
||||
case .random:
|
||||
rotation = CGFloat.random(in: 0...360) * .pi / 180
|
||||
}
|
||||
let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation)
|
||||
stroke.vertices.append(contentsOf: quad.vertices)
|
||||
stroke.vertexCount = stroke.vertices.endIndex
|
||||
}
|
||||
|
||||
private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) {
|
||||
let distance = start.distance(to: end)
|
||||
let factor: CGFloat
|
||||
switch configuration.granularity {
|
||||
case .fixed:
|
||||
factor = 1 / (stroke.thickness * stroke.style.stepRate)
|
||||
case .none:
|
||||
factor = 1 / (stroke.thickness * 10 / 500)
|
||||
}
|
||||
let segements = max(Int(distance * factor), 1)
|
||||
for i in 0..<segements {
|
||||
let t = CGFloat(i) / CGFloat(segements)
|
||||
let x = pow(1 - t, 2) * start.x + 2.0 * (1 - t) * t * control.x + t * t * end.x
|
||||
let y = pow(1 - t, 2) * start.y + 2.0 * (1 - t) * t * control.y + t * t * end.y
|
||||
let point = CGPoint(x: x, y: y)
|
||||
addPoint(point, on: stroke)
|
||||
}
|
||||
}
|
||||
|
||||
private func discardPoints(upto index: Int, on stroke: Stroke) {
|
||||
if index < 0 {
|
||||
stroke.vertices.removeAll()
|
||||
} else {
|
||||
let count = stroke.vertices.endIndex
|
||||
let dropCount = count - (max(0, index) + 1)
|
||||
stroke.vertices.removeLast(dropCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SolidPointStrokeGenerator {
|
||||
struct Configuration {
|
||||
var rotation: Rotation = .fixed
|
||||
var granularity: Granularity = .none
|
||||
}
|
||||
|
||||
enum Rotation {
|
||||
case fixed
|
||||
case random
|
||||
}
|
||||
|
||||
enum Granularity {
|
||||
case fixed
|
||||
case none
|
||||
}
|
||||
}
|
||||
112
Memola/Canvas/Geometries/Stroke/Stroke.swift
Normal file
112
Memola/Canvas/Geometries/Stroke/Stroke.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// Stroke.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class Stroke: Codable {
|
||||
var color: [CGFloat]
|
||||
var style: any PenStyle
|
||||
var thickness: CGFloat
|
||||
var angle: CGFloat = 0
|
||||
|
||||
var vertexIndex: Int = -1
|
||||
var keyPoints: [CGPoint] = []
|
||||
var thicknessFactor: CGFloat = 0.7
|
||||
|
||||
var vertices: [QuadVertex] = []
|
||||
var vertexBuffer: MTLBuffer?
|
||||
var vertexCount: Int = 0
|
||||
var tailVertices: [QuadVertex] = []
|
||||
|
||||
var texture: MTLTexture?
|
||||
// var strokeTexture: MTLTexture?
|
||||
|
||||
var isEmpty: Bool {
|
||||
vertices.isEmpty
|
||||
}
|
||||
|
||||
var isEraserPenStyle: Bool {
|
||||
style is EraserPenStyle
|
||||
}
|
||||
|
||||
init(color: [CGFloat], style: any PenStyle, thickness: CGFloat) {
|
||||
self.color = color
|
||||
self.style = style
|
||||
self.thickness = thickness
|
||||
}
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case color
|
||||
case style
|
||||
case thickness
|
||||
case vertices
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
color = try container.decode([CGFloat].self, forKey: .color)
|
||||
let style: String = try container.decode(String.self, forKey: .style)
|
||||
thickness = try container.decode(CGFloat.self, forKey: .thickness)
|
||||
vertices = try container.decode([QuadVertex].self, forKey: .vertices)
|
||||
vertexCount = vertices.count
|
||||
switch style {
|
||||
case "marker":
|
||||
self.style = .marker
|
||||
case "eraser":
|
||||
self.style = .eraser
|
||||
default:
|
||||
throw DecodingError.valueNotFound(PenStyle.self, .init(codingPath: [CodingKeys.style], debugDescription: "There is no pen style called `\(style)`."))
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(color, forKey: .color)
|
||||
try container.encode(thickness, forKey: .thickness)
|
||||
try container.encode(vertices, forKey: .vertices)
|
||||
let styleName: String
|
||||
switch style {
|
||||
case is MarkerPenStyle:
|
||||
styleName = "marker"
|
||||
case is EraserPenStyle:
|
||||
styleName = "eraser"
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
try container.encode(styleName, forKey: .style)
|
||||
}
|
||||
|
||||
func begin(at point: CGPoint) {
|
||||
style.generator.begin(at: point, on: self)
|
||||
}
|
||||
|
||||
func append(to point: CGPoint) {
|
||||
style.generator.append(to: point, on: self)
|
||||
}
|
||||
|
||||
func finish(at point: CGPoint) {
|
||||
style.generator.finish(at: point, on: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Stroke: Drawable {
|
||||
func prepare(device: MTLDevice) {
|
||||
if texture == nil {
|
||||
texture = style.loadTexture(on: device)
|
||||
}
|
||||
vertexBuffer = device.makeBuffer(bytes: &vertices, length: MemoryLayout<QuadVertex>.stride * vertexCount, options: .cpuCacheModeWriteCombined)
|
||||
}
|
||||
|
||||
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
guard !isEmpty else { return }
|
||||
prepare(device: device)
|
||||
renderEncoder.setFragmentTexture(texture, index: 0)
|
||||
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)
|
||||
}
|
||||
}
|
||||
18
Memola/Canvas/Geometries/Stroke/StrokeGenerator.swift
Normal file
18
Memola/Canvas/Geometries/Stroke/StrokeGenerator.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// StrokeGenerator.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol StrokeGenerator {
|
||||
associatedtype Configuration
|
||||
|
||||
var configuration: Configuration { get set }
|
||||
|
||||
func begin(at point: CGPoint, on stroke: Stroke)
|
||||
func append(to point: CGPoint, on stroke: Stroke)
|
||||
func finish(at point: CGPoint, on stroke: Stroke)
|
||||
}
|
||||
51
Memola/Canvas/History/History.swift
Normal file
51
Memola/Canvas/History/History.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// History.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
class History: ObservableObject {
|
||||
@Published var undoStack: [HistoryEvent] = []
|
||||
@Published var redoStack: [HistoryEvent] = []
|
||||
|
||||
let historyPublisher = PassthroughSubject<HistoryAction, Never>()
|
||||
|
||||
var undoDisabled: Bool {
|
||||
undoStack.isEmpty
|
||||
}
|
||||
var redoDisabled: Bool {
|
||||
redoStack.isEmpty
|
||||
}
|
||||
|
||||
func undo() -> Bool {
|
||||
guard let event = undoStack.popLast() else {
|
||||
return false
|
||||
}
|
||||
addRedo(event)
|
||||
return true
|
||||
}
|
||||
|
||||
func redo() -> HistoryEvent? {
|
||||
guard let event = redoStack.popLast() else {
|
||||
return nil
|
||||
}
|
||||
addUndo(event)
|
||||
return event
|
||||
}
|
||||
|
||||
func addUndo(_ event: HistoryEvent) {
|
||||
undoStack.append(event)
|
||||
}
|
||||
|
||||
func addRedo(_ event: HistoryEvent) {
|
||||
redoStack.append(event)
|
||||
}
|
||||
|
||||
func resetRedo() {
|
||||
redoStack.removeAll()
|
||||
}
|
||||
}
|
||||
13
Memola/Canvas/History/HistoryAction.swift
Normal file
13
Memola/Canvas/History/HistoryAction.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// HistoryAction.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum HistoryAction {
|
||||
case undo
|
||||
case redo
|
||||
}
|
||||
12
Memola/Canvas/History/HistoryEvent.swift
Normal file
12
Memola/Canvas/History/HistoryEvent.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// HistoryEvent.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum HistoryEvent {
|
||||
case stroke(Stroke)
|
||||
}
|
||||
87
Memola/Canvas/RenderPasses/CacheRenderPass.swift
Normal file
87
Memola/Canvas/RenderPasses/CacheRenderPass.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// CacheRenderPass.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class CacheRenderPass: RenderPass {
|
||||
var label: String = "Cache Render Pass"
|
||||
|
||||
var descriptor: MTLRenderPassDescriptor?
|
||||
|
||||
var cachePipelineState: MTLComputePipelineState?
|
||||
var graphicPipelineState: MTLRenderPipelineState?
|
||||
|
||||
weak var graphicTexture: MTLTexture?
|
||||
var cacheTexture: MTLTexture?
|
||||
|
||||
weak var strokeRenderPass: StrokeRenderPass?
|
||||
weak var eraserRenderPass: EraserRenderPass?
|
||||
var clearsTexture: Bool = true
|
||||
|
||||
init(renderer: Renderer) {
|
||||
descriptor = MTLRenderPassDescriptor()
|
||||
cachePipelineState = PipelineStates.createCachePipelineState(from: renderer)
|
||||
}
|
||||
|
||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
|
||||
guard size != .zero else { return }
|
||||
cacheTexture = Textures.createCacheTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
|
||||
}
|
||||
|
||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||
guard let descriptor, let strokeRenderPass, let eraserRenderPass else { return }
|
||||
|
||||
copyTexture(on: canvas, with: renderer)
|
||||
|
||||
guard let graphicPipelineState else { return }
|
||||
descriptor.colorAttachments[0].texture = cacheTexture
|
||||
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||
descriptor.colorAttachments[0].storeAction = .store
|
||||
|
||||
if let stroke = canvas.graphicContext.currentStroke {
|
||||
if stroke.isEraserPenStyle {
|
||||
eraserRenderPass.stroke = stroke
|
||||
eraserRenderPass.descriptor = descriptor
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
} else {
|
||||
canvas.setGraphicRenderType(.inProgress)
|
||||
strokeRenderPass.stroke = stroke
|
||||
strokeRenderPass.graphicDescriptor = descriptor
|
||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||
}
|
||||
clearsTexture = false
|
||||
}
|
||||
}
|
||||
|
||||
private func copyTexture(on canvas: Canvas, with renderer: Renderer) {
|
||||
guard let graphicTexture, let cacheTexture else { return }
|
||||
guard let cachePipelineState else { return }
|
||||
guard let copyCommandBuffer = renderer.commandQueue.makeCommandBuffer() else {
|
||||
return
|
||||
}
|
||||
guard let computeEncoder = copyCommandBuffer.makeComputeCommandEncoder() else {
|
||||
return
|
||||
}
|
||||
computeEncoder.label = label
|
||||
|
||||
computeEncoder.setComputePipelineState(cachePipelineState)
|
||||
computeEncoder.setTexture(graphicTexture, index: 0)
|
||||
computeEncoder.setTexture(cacheTexture, index: 1)
|
||||
let threadgroupSize = MTLSize(width: 8, height: 8, depth: 1)
|
||||
let threadgroupCount = MTLSize(
|
||||
width: (graphicTexture.width + threadgroupSize.width - 1) / threadgroupSize.width,
|
||||
height: (graphicTexture.height + threadgroupSize.height - 1) / threadgroupSize.height,
|
||||
depth: 1
|
||||
)
|
||||
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
|
||||
computeEncoder.endEncoding()
|
||||
copyCommandBuffer.commit()
|
||||
}
|
||||
}
|
||||
45
Memola/Canvas/RenderPasses/EraserRenderPass.swift
Normal file
45
Memola/Canvas/RenderPasses/EraserRenderPass.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// EraserRenderPass.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class EraserRenderPass: RenderPass {
|
||||
var label: String = "Eraser Render Pass"
|
||||
|
||||
var descriptor: MTLRenderPassDescriptor?
|
||||
|
||||
var eraserPipelineState: MTLRenderPipelineState?
|
||||
|
||||
var stroke: Stroke?
|
||||
weak var graphicTexture: MTLTexture?
|
||||
|
||||
init(renderer: Renderer) {
|
||||
eraserPipelineState = PipelineStates.createEraserPipelineState(from: renderer)
|
||||
}
|
||||
|
||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
||||
|
||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||
guard let descriptor else { return }
|
||||
|
||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||
commandBuffer.label = "Eraser Command Buffer"
|
||||
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
|
||||
renderEncoder.label = label
|
||||
|
||||
guard let eraserPipelineState else { return }
|
||||
renderEncoder.setRenderPipelineState(eraserPipelineState)
|
||||
|
||||
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
||||
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
commandBuffer.commit()
|
||||
}
|
||||
}
|
||||
86
Memola/Canvas/RenderPasses/GraphicRenderPass.swift
Normal file
86
Memola/Canvas/RenderPasses/GraphicRenderPass.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// GraphicRenderPass.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class GraphicRenderPass: RenderPass {
|
||||
var label: String { "Graphic Render Pass" }
|
||||
var descriptor: MTLRenderPassDescriptor?
|
||||
var graphicTexture: MTLTexture?
|
||||
|
||||
var graphicPipelineState: MTLRenderPipelineState?
|
||||
|
||||
weak var strokeRenderPass: StrokeRenderPass?
|
||||
weak var eraserRenderPass: EraserRenderPass?
|
||||
|
||||
var clearsTexture: Bool = true
|
||||
|
||||
init(renderer: Renderer) {
|
||||
descriptor = MTLRenderPassDescriptor()
|
||||
graphicPipelineState = PipelineStates.createGraphicPipelineState(from: renderer)
|
||||
}
|
||||
|
||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
|
||||
guard size != .zero else { return }
|
||||
graphicTexture = Textures.createGraphicTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
|
||||
clearsTexture = true
|
||||
}
|
||||
|
||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||
guard let strokeRenderPass, let eraserRenderPass else { return }
|
||||
guard let descriptor else { return }
|
||||
|
||||
guard let graphicPipelineState else { return }
|
||||
let graphicContext = canvas.graphicContext
|
||||
guard let graphicTexture else { return }
|
||||
|
||||
descriptor.colorAttachments[0].texture = graphicTexture
|
||||
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||
descriptor.colorAttachments[0].storeAction = .store
|
||||
|
||||
if renderer.redrawsGraphicRender {
|
||||
canvas.setGraphicRenderType(.finished)
|
||||
for stroke in graphicContext.strokes {
|
||||
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
||||
continue
|
||||
}
|
||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||
clearsTexture = false
|
||||
if stroke.isEraserPenStyle {
|
||||
eraserRenderPass.stroke = stroke
|
||||
eraserRenderPass.descriptor = descriptor
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
} else {
|
||||
canvas.setGraphicRenderType(.finished)
|
||||
strokeRenderPass.stroke = stroke
|
||||
strokeRenderPass.graphicDescriptor = descriptor
|
||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||
}
|
||||
}
|
||||
renderer.redrawsGraphicRender = false
|
||||
}
|
||||
|
||||
if let stroke = graphicContext.previousStroke {
|
||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||
clearsTexture = false
|
||||
if stroke.isEraserPenStyle {
|
||||
eraserRenderPass.stroke = stroke
|
||||
eraserRenderPass.descriptor = descriptor
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
} else {
|
||||
canvas.setGraphicRenderType(.newlyFinished)
|
||||
strokeRenderPass.stroke = stroke
|
||||
strokeRenderPass.graphicDescriptor = descriptor
|
||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||
}
|
||||
graphicContext.previousStroke = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
78
Memola/Canvas/RenderPasses/StrokeRenderPass.swift
Normal file
78
Memola/Canvas/RenderPasses/StrokeRenderPass.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// StrokeRenderPass.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class StrokeRenderPass: RenderPass {
|
||||
var label: String = "Stroke Render Pass"
|
||||
|
||||
var descriptor: MTLRenderPassDescriptor?
|
||||
weak var graphicDescriptor: MTLRenderPassDescriptor?
|
||||
|
||||
var strokePipelineState: MTLRenderPipelineState?
|
||||
weak var graphicPipelineState: MTLRenderPipelineState?
|
||||
|
||||
var stroke: Stroke?
|
||||
var strokeTexture: MTLTexture?
|
||||
|
||||
init(renderer: Renderer) {
|
||||
descriptor = MTLRenderPassDescriptor()
|
||||
strokePipelineState = PipelineStates.createStrokePipelineState(from: renderer)
|
||||
}
|
||||
|
||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
|
||||
guard size != .zero else { return }
|
||||
strokeTexture = Textures.createStrokeTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
|
||||
}
|
||||
|
||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||
guard let descriptor else { return }
|
||||
|
||||
guard let strokeTexture else { return }
|
||||
descriptor.colorAttachments[0].texture = strokeTexture
|
||||
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||
descriptor.colorAttachments[0].loadAction = .clear
|
||||
descriptor.colorAttachments[0].storeAction = .store
|
||||
|
||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||
commandBuffer.label = "Stroke Command Buffer"
|
||||
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
|
||||
renderEncoder.label = label
|
||||
|
||||
guard let strokePipelineState else { return }
|
||||
renderEncoder.setRenderPipelineState(strokePipelineState)
|
||||
|
||||
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
||||
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
commandBuffer.commit()
|
||||
|
||||
drawStrokeTexture(on: canvas, with: renderer)
|
||||
}
|
||||
|
||||
func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) {
|
||||
guard let stroke else { return }
|
||||
guard let graphicDescriptor, let graphicPipelineState else { return }
|
||||
|
||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||
commandBuffer.label = "Graphic Command Buffer"
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: graphicDescriptor) else { return }
|
||||
renderEncoder.label = "Graphic Render Pass"
|
||||
renderEncoder.setRenderPipelineState(graphicPipelineState)
|
||||
|
||||
renderEncoder.setFragmentTexture(strokeTexture, index: 0)
|
||||
var uniforms = GraphicUniforms(color: stroke.color)
|
||||
let uniformsBuffer = renderer.device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
||||
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||
canvas.renderGraphic(device: renderer.device, renderEncoder: renderEncoder)
|
||||
renderEncoder.endEncoding()
|
||||
commandBuffer.commit()
|
||||
}
|
||||
}
|
||||
74
Memola/Canvas/RenderPasses/ViewPortRenderPass.swift
Normal file
74
Memola/Canvas/RenderPasses/ViewPortRenderPass.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ViewPortRenderPass.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class ViewPortRenderPass: RenderPass {
|
||||
var label: String { "View Port Render Pass"}
|
||||
var descriptor: MTLRenderPassDescriptor?
|
||||
|
||||
var gridPipelineState: MTLRenderPipelineState?
|
||||
var viewPortPipelineState: MTLRenderPipelineState?
|
||||
var viewPortUpdatePipelineState: MTLRenderPipelineState?
|
||||
|
||||
weak var cacheTexture: MTLTexture?
|
||||
|
||||
weak var view: MTKView?
|
||||
|
||||
init(renderer: Renderer) {
|
||||
gridPipelineState = PipelineStates.createGridPipelineState(from: renderer)
|
||||
viewPortPipelineState = PipelineStates.createViewPortPipelineState(from: renderer)
|
||||
viewPortUpdatePipelineState = PipelineStates.createViewPortPipelineState(from: renderer, isUpdate: true)
|
||||
}
|
||||
|
||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
||||
|
||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||
guard let descriptor else {
|
||||
return
|
||||
}
|
||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else {
|
||||
return
|
||||
}
|
||||
commandBuffer.label = "View Port Command Buffer"
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
|
||||
return
|
||||
}
|
||||
renderEncoder.label = label
|
||||
|
||||
guard let gridPipelineState else { return }
|
||||
renderEncoder.setRenderPipelineState(gridPipelineState)
|
||||
canvas.renderGrid(device: renderer.device, renderEncoder: renderEncoder)
|
||||
|
||||
if renderer.updatesViewPort {
|
||||
guard let viewPortUpdatePipelineState else {
|
||||
return
|
||||
}
|
||||
|
||||
renderEncoder.setRenderPipelineState(viewPortUpdatePipelineState)
|
||||
renderEncoder.setFragmentTexture(cacheTexture, index: 0)
|
||||
canvas.renderViewPortUpdate(device: renderer.device, renderEncoder: renderEncoder)
|
||||
} else {
|
||||
guard let viewPortPipelineState else {
|
||||
return
|
||||
}
|
||||
|
||||
renderEncoder.setRenderPipelineState(viewPortPipelineState)
|
||||
renderEncoder.setFragmentTexture(cacheTexture, index: 0)
|
||||
canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder)
|
||||
}
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
guard let drawable = view?.currentDrawable else {
|
||||
return
|
||||
}
|
||||
commandBuffer.present(drawable)
|
||||
commandBuffer.commit()
|
||||
}
|
||||
}
|
||||
18
Memola/Canvas/Shaders/Cache.metal
Normal file
18
Memola/Canvas/Shaders/Cache.metal
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Cache.metal
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
kernel void copy_texture_viewport(
|
||||
texture2d<float, access::read> inTexture [[texture(0)]],
|
||||
texture2d<float, access::write> outTexture [[texture(1)]],
|
||||
uint2 gid [[thread_position_in_grid]]
|
||||
) {
|
||||
float4 color = inTexture.read(gid);
|
||||
outTexture.write(color, gid);
|
||||
}
|
||||
46
Memola/Canvas/Shaders/Graphic.metal
Normal file
46
Memola/Canvas/Shaders/Graphic.metal
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Graphic.metal
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct VertexIn {
|
||||
float4 position [[position]];
|
||||
float2 textCoord;
|
||||
};
|
||||
|
||||
struct VertexOut {
|
||||
float4 position [[position]];
|
||||
float2 textCoord;
|
||||
float4 color;
|
||||
};
|
||||
|
||||
struct Uniforms {
|
||||
float4 color;
|
||||
};
|
||||
|
||||
vertex VertexOut vertex_graphic(
|
||||
constant VertexIn *vertices [[buffer(0)]],
|
||||
constant Uniforms &uniforms [[buffer(11)]],
|
||||
uint vertexId [[vertex_id]]
|
||||
) {
|
||||
VertexIn in = vertices[vertexId];
|
||||
VertexOut out;
|
||||
out.position = in.position;
|
||||
out.textCoord = in.textCoord;
|
||||
out.color = uniforms.color;
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 fragment_graphic(
|
||||
VertexOut out [[stage_in]],
|
||||
texture2d<float> offscreenTexture [[texture(0)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||
float4 color = float4(offscreenTexture.sample(textureSampler, out.textCoord));
|
||||
return float4(out.color.rgb, color.a * out.color.a);
|
||||
}
|
||||
44
Memola/Canvas/Shaders/Grid.metal
Normal file
44
Memola/Canvas/Shaders/Grid.metal
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Grid.metal
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct Vertex {
|
||||
float4 position [[position]];
|
||||
float pointSize [[point_size]];
|
||||
};
|
||||
|
||||
struct Uniforms {
|
||||
float ratio;
|
||||
float zoom;
|
||||
float4x4 transform;
|
||||
};
|
||||
|
||||
vertex Vertex vertex_grid(
|
||||
constant Vertex *vertices [[buffer(0)]],
|
||||
constant Uniforms &uniforms [[buffer(11)]],
|
||||
uint vertexId [[vertex_id]]
|
||||
) {
|
||||
Vertex _vertex = vertices[vertexId];
|
||||
float x = _vertex.position.x * uniforms.ratio;
|
||||
float y = _vertex.position.y * uniforms.ratio;
|
||||
float4 position = float4(x, y, 0, 1);
|
||||
_vertex.position = uniforms.transform * position;
|
||||
_vertex.pointSize = 10 * uniforms.zoom / 12;
|
||||
return _vertex;
|
||||
}
|
||||
|
||||
fragment float4 fragment_grid(
|
||||
Vertex _vertex [[stage_in]],
|
||||
float2 pointCoord [[point_coord]]
|
||||
) {
|
||||
float dist = length(pointCoord - float2(0.5));
|
||||
float4 color = float4(0.752, 0.752, 0.752, 1);
|
||||
color.a = 1.0 - smoothstep(0.4, 0.5, dist);
|
||||
return color;
|
||||
}
|
||||
55
Memola/Canvas/Shaders/Stroke.metal
Normal file
55
Memola/Canvas/Shaders/Stroke.metal
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Stroke.metal
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct VertexIn {
|
||||
float4 position [[position]];
|
||||
float2 textCoord;
|
||||
float4 color;
|
||||
float2 origin;
|
||||
float rotation;
|
||||
};
|
||||
|
||||
struct VertexOut {
|
||||
float4 position [[position]];
|
||||
float2 textCoord;
|
||||
float4 color;
|
||||
};
|
||||
|
||||
struct Uniforms {
|
||||
float4x4 transform;
|
||||
};
|
||||
|
||||
vertex VertexOut vertex_stroke(
|
||||
constant VertexIn *vertices [[buffer(0)]],
|
||||
constant Uniforms &uniforms [[buffer(11)]],
|
||||
uint vertexId [[vertex_id]]
|
||||
) {
|
||||
VertexIn in = vertices[vertexId];
|
||||
|
||||
float2 rotatedPosition;
|
||||
rotatedPosition.x = cos(in.rotation) * (in.position.x - in.origin.x) - sin(in.rotation) * (in.position.y - in.origin.y) + in.origin.x;
|
||||
rotatedPosition.y = sin(in.rotation) * (in.position.x - in.origin.x) + cos(in.rotation) * (in.position.y - in.origin.y) + in.origin.y;
|
||||
|
||||
VertexOut out;
|
||||
out.position = uniforms.transform * float4(rotatedPosition, 0, 1);
|
||||
out.textCoord = in.textCoord;
|
||||
out.color = in.color;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 fragment_stroke(
|
||||
VertexOut out [[stage_in]],
|
||||
texture2d<float> texture [[texture(0)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||
float4 color = float4(texture.sample(textureSampler, out.textCoord));
|
||||
return float4(1, 1, 1, color.a);
|
||||
}
|
||||
45
Memola/Canvas/Shaders/ViewPort.metal
Normal file
45
Memola/Canvas/Shaders/ViewPort.metal
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Viewport.metal
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct Vertex {
|
||||
float4 position [[position]];
|
||||
float2 textCoord;
|
||||
};
|
||||
|
||||
struct Uniforms {
|
||||
float4x4 transform;
|
||||
};
|
||||
|
||||
vertex Vertex vertex_viewport(
|
||||
constant Vertex *vertices [[buffer(0)]],
|
||||
uint vertexId [[vertex_id]]
|
||||
) {
|
||||
Vertex _vertex = vertices[vertexId];
|
||||
return _vertex;
|
||||
}
|
||||
|
||||
vertex Vertex vertex_viewport_update(
|
||||
constant Vertex *vertices [[buffer(0)]],
|
||||
constant Uniforms &uniforms [[buffer(11)]],
|
||||
uint vertexId [[vertex_id]]
|
||||
) {
|
||||
Vertex _vertex = vertices[vertexId];
|
||||
_vertex.position = uniforms.transform * _vertex.position;
|
||||
return _vertex;
|
||||
}
|
||||
|
||||
fragment float4 fragment_viewport(
|
||||
Vertex _vertex [[stage_in]],
|
||||
texture2d<float> offscreenTexture [[texture(0)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||
float4 sampledColor = float4(offscreenTexture.sample(textureSampler, _vertex.textCoord));
|
||||
return sampledColor;
|
||||
}
|
||||
27
Memola/Canvas/Tool/Pen/Pen.swift
Normal file
27
Memola/Canvas/Tool/Pen/Pen.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Pen.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
class Pen: NSObject, ObservableObject, Identifiable {
|
||||
@Published var style: PenStyle
|
||||
@Published var color: [CGFloat]
|
||||
@Published var thickness: CGFloat
|
||||
|
||||
init(style: any PenStyle, color: [CGFloat], thickness: CGFloat) {
|
||||
self.style = style
|
||||
self.color = color
|
||||
self.thickness = thickness
|
||||
}
|
||||
}
|
||||
|
||||
extension Pen {
|
||||
convenience init(for style: any PenStyle) {
|
||||
self.init(style: style, color: style.color, thickness: style.thinkness.min)
|
||||
}
|
||||
}
|
||||
25
Memola/Canvas/Tool/Pen/PenStyle.swift
Normal file
25
Memola/Canvas/Tool/Pen/PenStyle.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// PenStyle.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
protocol PenStyle {
|
||||
var icon: (base: String, tip: String?) { get }
|
||||
var textureName: String { get }
|
||||
var thinkness: (min: CGFloat, max: CGFloat) { get }
|
||||
var color: [CGFloat] { get }
|
||||
var stepRate: CGFloat { get }
|
||||
var generator: any StrokeGenerator { get }
|
||||
}
|
||||
|
||||
extension PenStyle {
|
||||
@discardableResult
|
||||
func loadTexture(on device: MTLDevice) -> MTLTexture? {
|
||||
Textures.createPenTexture(with: textureName, on: device)
|
||||
}
|
||||
}
|
||||
30
Memola/Canvas/Tool/PenStyles/EraserPenStyle.swift
Normal file
30
Memola/Canvas/Tool/PenStyles/EraserPenStyle.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// EraserPenStyle.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EraserPenStyle: PenStyle {
|
||||
var icon: (base: String, tip: String?) = ("eraser", nil)
|
||||
|
||||
var textureName: String = "point-texture"
|
||||
|
||||
var thinkness: (min: CGFloat, max: CGFloat) = (15, 120)
|
||||
|
||||
var color: [CGFloat] = [1, 1, 1, 0]
|
||||
|
||||
var stepRate: CGFloat = 0.2
|
||||
|
||||
var generator: any StrokeGenerator {
|
||||
SolidPointStrokeGenerator(configuration: .init())
|
||||
}
|
||||
}
|
||||
|
||||
extension PenStyle where Self == EraserPenStyle {
|
||||
static var eraser: PenStyle {
|
||||
EraserPenStyle()
|
||||
}
|
||||
}
|
||||
30
Memola/Canvas/Tool/PenStyles/MarkerPenStyle.swift
Normal file
30
Memola/Canvas/Tool/PenStyles/MarkerPenStyle.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// MarkerPenStyle.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MarkerPenStyle: PenStyle {
|
||||
var icon: (base: String, tip: String?) = ("marker-base", "marker-tip")
|
||||
|
||||
var textureName: String = "point-texture"
|
||||
|
||||
var thinkness: (min: CGFloat, max: CGFloat) = (15, 120)
|
||||
|
||||
var color: [CGFloat] = [1, 0.38, 0.38, 1]
|
||||
|
||||
var stepRate: CGFloat = 0.2
|
||||
|
||||
var generator: any StrokeGenerator {
|
||||
SolidPointStrokeGenerator(configuration: .init())
|
||||
}
|
||||
}
|
||||
|
||||
extension PenStyle where Self == MarkerPenStyle {
|
||||
static var marker: PenStyle {
|
||||
MarkerPenStyle()
|
||||
}
|
||||
}
|
||||
26
Memola/Canvas/Tool/Tool.swift
Normal file
26
Memola/Canvas/Tool/Tool.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Tool.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
class Tool: NSObject, ObservableObject {
|
||||
@Published var pens: [Pen]
|
||||
@Published var selectedPen: Pen?
|
||||
|
||||
override init() {
|
||||
pens = [
|
||||
Pen(for: .marker),
|
||||
Pen(for: .eraser)
|
||||
]
|
||||
super.init()
|
||||
}
|
||||
|
||||
func changePen(_ pen: Pen) {
|
||||
selectedPen = pen
|
||||
}
|
||||
}
|
||||
331
Memola/Canvas/View/Bridge/CanvasViewController.swift
Normal file
331
Memola/Canvas/View/Bridge/CanvasViewController.swift
Normal file
@@ -0,0 +1,331 @@
|
||||
//
|
||||
// CanvasViewController.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class CanvasViewController: UIViewController {
|
||||
let drawingView: DrawingView
|
||||
let scrollView: UIScrollView = UIScrollView()
|
||||
var renderView: MTKView {
|
||||
drawingView.renderView
|
||||
}
|
||||
|
||||
let tool: Tool
|
||||
let canvas: Canvas
|
||||
let history: History
|
||||
let renderer: Renderer
|
||||
|
||||
var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
init(tool: Tool, canvas: Canvas, history: History) {
|
||||
self.tool = tool
|
||||
self.canvas = canvas
|
||||
self.history = history
|
||||
self.drawingView = DrawingView(tool: tool, canvas: canvas, history: history)
|
||||
self.renderer = Renderer(canvasView: drawingView.renderView)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
configureViews()
|
||||
configureGestures()
|
||||
configureListeners()
|
||||
|
||||
loadBoard()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
resizeDocumentView()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
drawingView.disableUserInteraction()
|
||||
drawingView.updateDrawableSize(with: view.frame.size)
|
||||
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||
renderView.draw()
|
||||
drawingView.enableUserInteraction()
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController {
|
||||
func configureViews() {
|
||||
view.backgroundColor = .white
|
||||
renderView.autoResizeDrawable = false
|
||||
renderView.enableSetNeedsDisplay = true
|
||||
renderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
renderView.clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 1)
|
||||
view.addSubview(renderView)
|
||||
NSLayoutConstraint.activate([
|
||||
renderView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
renderView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
renderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
renderView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||
])
|
||||
|
||||
scrollView.maximumZoomScale = canvas.maximumZoomScale
|
||||
scrollView.minimumZoomScale = canvas.minimumZoomScale
|
||||
scrollView.contentInsetAdjustmentBehavior = .never
|
||||
scrollView.isScrollEnabled = true
|
||||
scrollView.showsVerticalScrollIndicator = true
|
||||
scrollView.showsHorizontalScrollIndicator = true
|
||||
scrollView.delegate = self
|
||||
scrollView.backgroundColor = .clear
|
||||
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(scrollView)
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||
])
|
||||
|
||||
scrollView.addSubview(drawingView)
|
||||
drawingView.backgroundColor = .clear
|
||||
drawingView.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
func resizeDocumentView(to newSize: CGSize? = nil) {
|
||||
scrollView.layoutIfNeeded()
|
||||
|
||||
let size = canvas.size
|
||||
let widthScale = (newSize?.width ?? view.frame.width) / size.width
|
||||
let heightScale = (newSize?.height ?? view.frame.height) / size.height
|
||||
let scale = max(widthScale, heightScale)
|
||||
|
||||
let width = size.width * scale
|
||||
let height = size.height * scale
|
||||
let newFrame = CGRect(x: 0, y: 0, width: width, height: height)
|
||||
drawingView.frame = newFrame
|
||||
|
||||
scrollView.setZoomScale(canvas.minimumZoomScale, animated: true)
|
||||
centerDocumentView(to: newSize)
|
||||
|
||||
let offsetX = (newFrame.width * canvas.minimumZoomScale - view.frame.width) / 2
|
||||
let offsetY = (newFrame.height * canvas.minimumZoomScale - view.frame.height) / 2
|
||||
|
||||
let point = CGPoint(x: offsetX, y: offsetY)
|
||||
scrollView.setContentOffset(point, animated: true)
|
||||
|
||||
drawingView.updateDrawableSize(with: view.frame.size)
|
||||
}
|
||||
|
||||
func centerDocumentView(to newSize: CGSize? = nil) {
|
||||
let documentViewSize = drawingView.frame.size
|
||||
let scrollViewSize = newSize ?? view.frame.size
|
||||
let verticalPadding = documentViewSize.height < scrollViewSize.height ? (scrollViewSize.height - documentViewSize.height) / 2 : 0
|
||||
let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0
|
||||
self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController {
|
||||
func configureListeners() {
|
||||
canvas.$state
|
||||
.sink { [weak self] state in
|
||||
self?.canvasStateChanged(state)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
tool.$selectedPen
|
||||
.sink { [weak self] pen in
|
||||
self?.penChanged(to: pen)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
history.historyPublisher
|
||||
.sink { [weak self] action in
|
||||
switch action {
|
||||
case .undo:
|
||||
self?.historyUndid()
|
||||
case .redo:
|
||||
self?.historyRedid()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController {
|
||||
func loadBoard() {
|
||||
canvas.load()
|
||||
}
|
||||
|
||||
func canvasStateChanged(_ state: Canvas.State) {
|
||||
guard state == .loaded else { return }
|
||||
renderView.delegate = self
|
||||
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||
renderView.draw()
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController: MTKViewDelegate {
|
||||
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { }
|
||||
|
||||
func draw(in view: MTKView) {
|
||||
guard view.drawableSize != .zero else {
|
||||
return
|
||||
}
|
||||
canvas.updateTransform(on: drawingView)
|
||||
renderer.draw(in: view, on: canvas)
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController {
|
||||
func configureGestures() {
|
||||
let drawingPanGesture = UIPanGestureRecognizer(target: self, action: #selector(recognizePanGesture))
|
||||
drawingPanGesture.maximumNumberOfTouches = 1
|
||||
drawingPanGesture.minimumNumberOfTouches = 1
|
||||
drawingView.addGestureRecognizer(drawingPanGesture)
|
||||
|
||||
let drawingTapGesture = UITapGestureRecognizer(target: self, action: #selector(recognizeTapGesture))
|
||||
drawingTapGesture.numberOfTapsRequired = 1
|
||||
drawingView.addGestureRecognizer(drawingTapGesture)
|
||||
}
|
||||
|
||||
@objc func recognizePanGesture(_ gesture: UIPanGestureRecognizer) {
|
||||
let point = gesture.location(in: drawingView)
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
drawingView.touchBegan(on: point)
|
||||
case .changed:
|
||||
drawingView.touchMoved(to: point)
|
||||
case .ended:
|
||||
drawingView.touchEnded(to: point)
|
||||
case .cancelled:
|
||||
drawingView.touchEnded(to: point)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
|
||||
let point = gesture.location(in: drawingView)
|
||||
drawingView.touchBegan(on: point)
|
||||
drawingView.touchEnded(to: point)
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController: UIScrollViewDelegate {
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
drawingView
|
||||
}
|
||||
|
||||
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
||||
magnificationStarted()
|
||||
}
|
||||
|
||||
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
canvas.setZoomScale(scrollView.zoomScale)
|
||||
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||
renderView.draw()
|
||||
}
|
||||
|
||||
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||
centerDocumentView()
|
||||
magnificationEnded()
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
draggingStarted()
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||
renderView.draw()
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
if !scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
|
||||
scrollViewDidEndScrolling(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate, scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
|
||||
scrollViewDidEndScrolling(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
|
||||
draggingEnded()
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController {
|
||||
func magnificationStarted() {
|
||||
guard !renderer.updatesViewPort else { return }
|
||||
canvas.updateClipBounds(scrollView, on: drawingView)
|
||||
drawingView.disableUserInteraction()
|
||||
renderer.updatesViewPort = true
|
||||
}
|
||||
|
||||
func magnificationEnded() {
|
||||
renderer.updatesViewPort = false
|
||||
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||
renderView.draw()
|
||||
drawingView.enableUserInteraction()
|
||||
}
|
||||
|
||||
func draggingStarted() {
|
||||
guard !renderer.updatesViewPort else { return }
|
||||
canvas.updateClipBounds(scrollView, on: drawingView)
|
||||
drawingView.disableUserInteraction()
|
||||
renderer.updatesViewPort = true
|
||||
}
|
||||
|
||||
func draggingEnded() {
|
||||
renderer.updatesViewPort = false
|
||||
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||
renderView.draw()
|
||||
drawingView.enableUserInteraction()
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController {
|
||||
func penChanged(to pen: Pen?) {
|
||||
if let pen, let device = drawingView.renderView.device {
|
||||
pen.style.loadTexture(on: device)
|
||||
}
|
||||
let isPenSelected = pen != nil
|
||||
scrollView.isScrollEnabled = !isPenSelected
|
||||
drawingView.isUserInteractionEnabled = isPenSelected
|
||||
isPenSelected ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction()
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController {
|
||||
func historyUndid() {
|
||||
guard history.undo() else { return }
|
||||
drawingView.disableUserInteraction()
|
||||
canvas.graphicContext.undoGraphic()
|
||||
renderer.redrawsGraphicRender = true
|
||||
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||
renderView.draw()
|
||||
drawingView.enableUserInteraction()
|
||||
}
|
||||
|
||||
func historyRedid() {
|
||||
guard let event = history.redo() else { return }
|
||||
drawingView.disableUserInteraction()
|
||||
canvas.graphicContext.redoGraphic(for: event)
|
||||
renderer.redrawsGraphicRender = true
|
||||
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||
renderView.draw()
|
||||
drawingView.enableUserInteraction()
|
||||
}
|
||||
}
|
||||
68
Memola/Canvas/View/Bridge/DrawingView.swift
Normal file
68
Memola/Canvas/View/Bridge/DrawingView.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// DrawingView.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
class DrawingView: UIView {
|
||||
let tool: Tool
|
||||
let canvas: Canvas
|
||||
let history: History
|
||||
let renderView: MTKView
|
||||
|
||||
var ratio: CGFloat { canvas.size.width / bounds.width }
|
||||
|
||||
private var disablesUserInteraction: Bool = false
|
||||
|
||||
required init(tool: Tool, canvas: Canvas, history: History) {
|
||||
self.tool = tool
|
||||
self.canvas = canvas
|
||||
self.history = history
|
||||
self.renderView = MTKView(frame: .zero)
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateDrawableSize(with size: CGSize) {
|
||||
renderView.drawableSize = size.multiply(by: 2.5)
|
||||
}
|
||||
|
||||
func touchBegan(on point: CGPoint) {
|
||||
guard !disablesUserInteraction else { return }
|
||||
guard let pen = tool.selectedPen else { return }
|
||||
let stroke = canvas.beginTouch(at: point.muliply(by: ratio), pen: pen)
|
||||
renderView.draw()
|
||||
history.addUndo(.stroke(stroke))
|
||||
history.resetRedo()
|
||||
}
|
||||
|
||||
func touchMoved(to point: CGPoint) {
|
||||
guard !disablesUserInteraction else { return }
|
||||
canvas.moveTouch(to: point.muliply(by: ratio))
|
||||
renderView.draw()
|
||||
}
|
||||
|
||||
func touchEnded(to point: CGPoint) {
|
||||
guard !disablesUserInteraction else { return }
|
||||
canvas.endTouch(at: point.muliply(by: ratio))
|
||||
renderView.draw()
|
||||
}
|
||||
|
||||
func disableUserInteraction() {
|
||||
disablesUserInteraction = true
|
||||
}
|
||||
|
||||
func enableUserInteraction() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
|
||||
self?.disablesUserInteraction = false
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Memola/Canvas/View/CanvasView.swift
Normal file
20
Memola/Canvas/View/CanvasView.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// CanvasView.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CanvasView: UIViewControllerRepresentable {
|
||||
@EnvironmentObject var tool: Tool
|
||||
@EnvironmentObject var canvas: Canvas
|
||||
@EnvironmentObject var history: History
|
||||
|
||||
func makeUIViewController(context: Context) -> CanvasViewController {
|
||||
CanvasViewController(tool: tool, canvas: canvas, history: history)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: CanvasViewController, context: Context) { }
|
||||
}
|
||||
17
Memola/Extensions/Array++.swift
Normal file
17
Memola/Extensions/Array++.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Array++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Array {
|
||||
mutating func append(_ element: Element, capacity: Int) {
|
||||
if count >= capacity {
|
||||
remove(at: 0)
|
||||
}
|
||||
append(element)
|
||||
}
|
||||
}
|
||||
14
Memola/Extensions/CGAffineTransform++.swift
Normal file
14
Memola/Extensions/CGAffineTransform++.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// CGAffineTransform++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension CGAffineTransform {
|
||||
static func * (lhs: CGAffineTransform, rhs: CGAffineTransform) -> CGAffineTransform {
|
||||
return lhs.concatenating(rhs)
|
||||
}
|
||||
}
|
||||
14
Memola/Extensions/CGFloat++.swift
Normal file
14
Memola/Extensions/CGFloat++.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// CGFloat++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension CGFloat {
|
||||
var float: Float {
|
||||
Float(self)
|
||||
}
|
||||
}
|
||||
29
Memola/Extensions/CGPoint++.swift
Normal file
29
Memola/Extensions/CGPoint++.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// CGPoint++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension CGPoint {
|
||||
func muliply(by factor: CGFloat) -> CGPoint {
|
||||
CGPoint(x: x * factor, y: y * factor)
|
||||
}
|
||||
|
||||
func distance(to point: CGPoint) -> CGFloat {
|
||||
let p = pow(x - point.x, 2) + pow(y - point.y, 2)
|
||||
return sqrt(p)
|
||||
}
|
||||
|
||||
static func middle(p1: CGPoint, p2: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5)
|
||||
}
|
||||
|
||||
func angle(to point: CGPoint) -> CGFloat {
|
||||
let deltaX = point.x - x
|
||||
let deltaY = point.y - y
|
||||
return atan2(deltaY, deltaX)
|
||||
}
|
||||
}
|
||||
19
Memola/Extensions/CGRect++.swift
Normal file
19
Memola/Extensions/CGRect++.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// CGRect++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension CGRect {
|
||||
func transform(to rect: CGRect) -> CGAffineTransform {
|
||||
var t = CGAffineTransform.identity
|
||||
t = t.translatedBy(x: -self.minX, y: -self.minY)
|
||||
t = t.translatedBy(x: rect.minX, y: rect.minY)
|
||||
t = t.scaledBy(x: 1 / self.width, y: 1 / self.height)
|
||||
t = t.scaledBy(x: rect.width, y: rect.height)
|
||||
return t
|
||||
}
|
||||
}
|
||||
14
Memola/Extensions/CGSize++.swift
Normal file
14
Memola/Extensions/CGSize++.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// CGSize++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension CGSize {
|
||||
func multiply(by scale: CGFloat) -> CGSize {
|
||||
CGSize(width: width * scale, height: height * scale)
|
||||
}
|
||||
}
|
||||
21
Memola/Extensions/Collection++.swift
Normal file
21
Memola/Extensions/Collection++.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Collection++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Collection {
|
||||
subscript(safe index: Index) -> Element? {
|
||||
return indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection where Index == Int {
|
||||
subscript(fromEnd index: Index) -> Element? {
|
||||
let i = count - (index + 1)
|
||||
return self[safe: i]
|
||||
}
|
||||
}
|
||||
27
Memola/Extensions/Color++.swift
Normal file
27
Memola/Extensions/Color++.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Color++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
var components: [CGFloat] {
|
||||
let color = UIColor(self)
|
||||
return color.components
|
||||
}
|
||||
|
||||
static func rgba(from color: [CGFloat]) -> Color {
|
||||
Color(red: color[0], green: color[1], blue: color[2]).opacity(color[3])
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
var components: [CGFloat] {
|
||||
let uiColor: UIColor = self
|
||||
let ciColor: CIColor = .init(color: uiColor)
|
||||
return [ciColor.red, ciColor.green, ciColor.blue, ciColor.alpha]
|
||||
}
|
||||
}
|
||||
14
Memola/Extensions/MTLDevice++.swift
Normal file
14
Memola/Extensions/MTLDevice++.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// MTLDevice++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
|
||||
extension MTLDevice {
|
||||
func maximumTextureDimension() -> Int {
|
||||
supportsFamily(.apple3) ? 16384 : 8192
|
||||
}
|
||||
}
|
||||
21
Memola/Extensions/simd_float4x4++.swift
Normal file
21
Memola/Extensions/simd_float4x4++.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// simd_float4x4++.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
extension simd_float4x4 {
|
||||
init(_ transform: CGAffineTransform) {
|
||||
let t = CATransform3DMakeAffineTransform(transform)
|
||||
self = simd_float4x4([
|
||||
[Float(t.m11), Float(t.m12), Float(t.m13), Float(t.m14)],
|
||||
[Float(t.m21), Float(t.m22), Float(t.m23), Float(t.m24)],
|
||||
[Float(t.m31), Float(t.m32), Float(t.m33), Float(t.m34)],
|
||||
[Float(t.m41), Float(t.m42), Float(t.m43), Float(t.m44)]
|
||||
])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user