*/}}
Browse Source

Merge branch 'master' of https://www.wellobserve.com/repositories/chengdulittlea/OurPaint

YimingWu 1 year ago
parent
commit
a0312964fd
9 changed files with 338 additions and 145 deletions
  1. 1 1
      CMakeLists.txt
  2. 1 0
      GenerateAppimage.py
  3. 4 0
      README.md
  4. 2 2
      ournodes.c
  5. 97 141
      ouroperations.c
  6. 1 1
      ourpaint.c
  7. 19 0
      ourpaint.h
  8. 205 0
      ourshader.cpp
  9. 8 0
      ourtranslations.c

+ 1 - 1
CMakeLists.txt

@@ -38,7 +38,7 @@ include_directories(
 )
 
 file(GLOB_RECURSE OurPaintFiles 
-    ourpaint.c ouroperations.c ournodes.c ourtranslations.c
+    ourpaint.c ouroperations.c ournodes.c ourtranslations.c ourshader.cpp
     BuildResources/data_splash.c
     BuildResources/data_splash_highdpi.c
 )

+ 1 - 0
GenerateAppimage.py

@@ -62,6 +62,7 @@ additional="""
     - libfreetype6
     - liblcms2-2
     - libpng16-16
+    - libglew2.1
 """
 template=template.replace("---includes---",additional)
 

+ 4 - 0
README → README.md

@@ -6,6 +6,8 @@
 
 了解更多: https://ChengduLittleA.com/ourpaint
 
+赞助:https://patreon.com/ChengduLittleA
+
 好得涂 采用 GNU GPL v3 许可证,Noto 字体采用 SIL Open Font 许可证
 您将在源代码文件夹找到许可证的具体信息。
 
@@ -37,6 +39,8 @@ Copyright (C) 2022-2023 Wu Yiming
 
 Learn more about LaGUI: https://ChengduLittleA.com/lagui
 
+Support the development: https://patreon.com/ChengduLittleA
+
 Our Paint is licensed with GNU GPL v3, and Noto fonts are licensed with SIL Open Font license.
 You should be able to find details about the license in the source code directory.
 

+ 2 - 2
ournodes.c

@@ -123,7 +123,7 @@ void IDN_BrushOutputsDestroy(OurBrushOutputsNode* n){
 }
 int IDN_BrushOutputsVisit(OurBrushOutputsNode* n, laListHandle* l){
     LA_GUARD_THIS_NODE(n);
-#define BRUSH_OUT_VISIT(a)\
+#define BRUSH_OUT_VISIT(a) \
     if(LA_SRC_AND_PARENT(n->a)){ laBaseNode*bn=n->a->Source->Parent; LA_VISIT_NODE(bn); }
     BRUSH_OUT_VISIT(Offset)
     BRUSH_OUT_VISIT(Size)
@@ -141,7 +141,7 @@ int IDN_BrushOutputsVisit(OurBrushOutputsNode* n, laListHandle* l){
 }
 int IDN_BrushOutputsEval(OurBrushOutputsNode* n){
     if(!Our->CurrentBrush) return 0;
-#define BRUSH_OUT_EVAL(a)\
+#define BRUSH_OUT_EVAL(a) \
     if(LA_SRC_AND_PARENT(n->a) && (n->a->Source->DataType&LA_PROP_FLOAT)){ Our->CurrentBrush->Eval##a=*((real*)n->a->Source->Data); }
     if(LA_SRC_AND_PARENT(n->Offset) && (n->Offset->Source->DataType&LA_PROP_FLOAT|LA_PROP_ARRAY) && n->Offset->Source->ArrLen>=2){
         Our->CurrentBrush->EvalOffset[0]=((real*)n->Offset->Source->Data)[0];

+ 97 - 141
ouroperations.c

@@ -24,119 +24,6 @@ OurPaint *Our;
 extern LA MAIN;
 extern tnsMain* T;
 
-const char OUR_CANVAS_SHADER[]="#version 430\n\
-layout(local_size_x = 32, local_size_y = 32, local_size_z = 1) in;\n\
-layout(rgba16, binding = 0) uniform image2D img;\n\
-layout(rgba16, binding = 1) coherent uniform image2D smudge_buckets;\n\
-uniform ivec2 uBrushCorner;\n\
-uniform vec2 uBrushCenter;\n\
-uniform float uBrushSize;\n\
-uniform float uBrushHardness;\n\
-uniform float uBrushSmudge;\n\
-uniform float uBrushSlender;\n\
-uniform float uBrushAngle;\n\
-uniform float uBrushRecentness;\n\
-uniform vec4 uBrushColor;\n\
-uniform vec4 uBackgroundColor;\n\
-uniform int uBrushErasing;\n\
-const vec4 p1_22=vec4(1.0/2.2,1.0/2.2,1.0/2.2,1.0/2.2);\n\
-const vec4 p22=vec4(2.2,2.2,2.2,2.2);\n\
-float atan2(in float y, in float x){\n\
-    bool s = (abs(x) > abs(y)); return mix(3.1415926535/2.0 - atan(x,y), atan(y,x), s);\n\
-}\n\
-vec2 rotate(vec2 v, float angle) {\n\
-  float s = sin(angle); float c = cos(angle);\n\
-  return mat2(c,-s,s,c) * v;\n\
-}\n\
-float brightness(vec4 color) {\n\
-    return color.r*0.2126+color.b*0.7152+color.g*0.0722;\n\
-}\n\
-vec4 mix_over(vec4 colora, vec4 colorb){\n\
-    vec4 c; c.a=colora.a+colorb.a*(1-colora.a);\n\
-    c.rgb=(colora.rgb+colorb.rgb*(1-colora.a));\n\
-    return c;\n\
-}\n\
-int dab(float d, vec4 color, float size, float hardness, float smudge, vec4 smudge_color, vec4 last_color, out vec4 final){\n\
-    vec4 cc=color;\n\
-    float fac=1-pow(d/size,1+1/(1-hardness+1e-4));\n\
-    cc.a=color.a*fac*(1-smudge); cc.rgb=cc.rgb*cc.a;\n\
-    float erasing=float(uBrushErasing);\n\
-    cc=cc*(1-erasing);\n\
-    //vec4 c2=mix(last_color,smudge_color,smudge*fac*color.a);\n\
-    //c2=mix_over(cc,c2);\n\
-    vec4 c2=mix_over(cc,last_color);\n\
-    c2=mix(c2,smudge_color,smudge*fac*color.a);\n\
-    c2=mix(c2,c2*(1-fac*color.a),erasing);\n\
-    final=c2;\n\
-    return 1;\n\
-}\n\
-subroutine void BrushRoutines();\n\
-subroutine(BrushRoutines) void DoDabs(){\n\
-    ivec2 px = ivec2(gl_GlobalInvocationID.xy)+uBrushCorner;\n\
-    if(px.x<0||px.y<0||px.x>1024||px.y>1024) return;\n\
-    vec2 fpx=vec2(px);\n\
-    fpx=uBrushCenter+rotate(fpx-uBrushCenter,uBrushAngle);\n\
-    fpx.x=uBrushCenter.x+(fpx.x-uBrushCenter.x)*(1+uBrushSlender);\n\
-    float dd=distance(fpx,uBrushCenter); if(dd>uBrushSize) return;\n\
-    vec4 dabc=imageLoad(img, px);\n\
-    vec4 smudgec=pow(mix(pow(imageLoad(smudge_buckets,ivec2(1,0)),p1_22),pow(imageLoad(smudge_buckets,ivec2(0,0)),p1_22),uBrushRecentness),p22);\n\
-    vec4 final_color;\n\
-    dab(dd,uBrushColor,uBrushSize,uBrushHardness,uBrushSmudge,smudgec,dabc,final_color);\n\
-    dabc=final_color;\n\
-    imageStore(img, px, dabc);\n\
-}\n\
-subroutine(BrushRoutines) void DoSample(){\n\
-    ivec2 p=ivec2(gl_GlobalInvocationID.xy);\n\
-    int DoSample=1; vec4 color;\n\
-    if(p.y==0){\n\
-        vec2 sp=round(vec2(sin(float(p.x)),cos(float(p.x)))*uBrushSize);\n\
-        ivec2 px=ivec2(sp)+uBrushCorner; if(px.x<0||px.y<0||px.x>=1024||px.y>=1024){ DoSample=0; }\n\
-        if(DoSample!=0){\n\
-            ivec2 b=uBrushCorner; if(b.x>=0&&b.y>=0&&b.x<1024&&b.y<1024){ imageStore(smudge_buckets,ivec2(128+32,0),imageLoad(img, b)); }\n\
-            color=imageLoad(img, px);\n\
-            imageStore(smudge_buckets,ivec2(p.x+128,0),color);\n\
-        }\n\
-    }else{DoSample=0;}\n\
-    memoryBarrier();barrier(); if(DoSample==0) return;\n\
-    if(uBrushErasing==0 || p.x!=0) return;\n\
-    color=vec4(0,0,0,0); for(int i=0;i<32;i++){ color=color+imageLoad(smudge_buckets, ivec2(i+128,0)); }\n\
-    color=mix(color/32,imageLoad(smudge_buckets, ivec2(128+32,0)),0.6*(1-uBrushColor.a)); vec4 oldcolor=imageLoad(smudge_buckets, ivec2(0,0));\n\
-    imageStore(smudge_buckets,ivec2(1,0),uBrushErasing==2?color:oldcolor);\n\
-    imageStore(smudge_buckets,ivec2(0,0),color);\n\
-}\n\
-subroutine uniform BrushRoutines uBrushRoutineSelection;\n\
-\n\
-void main() {\n\
-    uBrushRoutineSelection();\n\
-}\n\
-";
-
-const char OUR_COMPOSITION_SHADER[]="#version 430\n\
-layout(local_size_x = 32, local_size_y = 32, local_size_z = 1) in;\n\
-layout(rgba16, binding = 0) uniform image2D top;\n\
-layout(rgba16, binding = 1) uniform image2D bottom;\n\
-uniform int uBlendMode;\n\
-uniform float uAlphaTop;\n\
-uniform float uAlphaBottom;\n\
-vec4 mix_over(vec4 colora, vec4 colorb){\n\
-    colora=colora*uAlphaTop/uAlphaBottom;\n\
-    vec4 c; c.a=colora.a+colorb.a*(1-colora.a);\n\
-    c.rgb=(colora.rgb+colorb.rgb*(1-colora.a));\n\
-    return c;\n\
-}\n\
-vec4 add_over(vec4 colora, vec4 colorb){\n\
-    colora=colora*uAlphaTop/uAlphaBottom;\n\
-    vec4 a=colora+colorb; a.a=clamp(a.a,0,1); return a;\n\
-}\n\
-void main() {\n\
-    ivec2 px=ivec2(gl_GlobalInvocationID.xy);\n\
-    vec4 c1=imageLoad(top,px); vec4 c2=imageLoad(bottom,px);\n\
-    vec4 c=(uBlendMode==0)?mix_over(c1,c2):add_over(c1,c2);\n\
-    imageStore(bottom,px,c);\n\
-    imageStore(top,px,vec4(0,0,0,0));\n\
-}";
-
-
 void our_LayerEnsureTiles(OurLayer* ol, real xmin,real xmax, real ymin,real ymax, int Aligned, int *tl, int *tr, int* tu, int* tb);
 void our_LayerEnsureTileDirect(OurLayer* ol, int col, int row);
 void our_RecordUndo(OurLayer* ol, real xmin,real xmax, real ymin,real ymax,int Aligned,int Push);
@@ -195,6 +82,7 @@ void our_InitColorProfiles(){
 void ourui_CanvasPanel(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *UNUSED, int context){
     laColumn* c=laFirstColumn(uil);
     laUiItem* ui=laShowCanvas(uil,c,0,"our.canvas",0,-1);
+    laCanvasExtra* ce=ui->Extra; ce->ZoomX=ce->ZoomY=1.0f/Our->DefaultScale;
 }
 void ourui_Layer(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *UNUSED, int context){
     laColumn* c=laFirstColumn(uil); laColumn* cl,*cr; laSplitColumn(uil,c,0.7); cl=laLeftColumn(c,0);cr=laRightColumn(c,1);
@@ -270,7 +158,7 @@ void ourui_ToolsPanel(laUiList *uil, laPropPack *This, laPropPack *DetachedProps
     laUiItem* b1, *b2;
 #define OUR_BR b1=laBeginRow(uil,c,0,0);
 #define OUR_ER laEndRow(uil,b1);
-#define OUR_PRESSURE(a)\
+#define OUR_PRESSURE(a) \
     b2=laOnConditionThat(uil,c,laNot(laPropExpression(0,"our.tools.current_brush.use_nodes")));\
     laShowItemFull(uil,c,0,"our.tools.current_brush." a,0,"text=P",0,0);\
     laEndCondition(uil,b2);
@@ -338,8 +226,10 @@ void ourui_ColorPanel(laUiList *uil, laPropPack *This, laPropPack *DetachedProps
     }laElse(uil,b);{
         laShowItemFull(uil,c,0,"our.current_color",LA_WIDGET_FLOAT_COLOR_HCY,0,0,0)->Flags|=LA_UI_FLAGS_COLOR_SPACE_CLAY;
     }laEndCondition(uil,b);
-
-    laShowItem(uil,c,0,"our.current_color");
+    b=laBeginRow(uil,c,0,0);
+    laShowItem(uil,c,0,"our.preferences.spectral_mode");
+    laShowItem(uil,c,0,"our.current_color")->Expand=1;
+    laEndRow(uil,b);
 }
 void ourui_BrushPage(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *UNUSED, int context){
     laColumn* c=laFirstColumn(uil); laColumn* cl,*cr; laSplitColumn(uil,c,0.5); cl=laLeftColumn(c,0);cr=laRightColumn(c,15);
@@ -391,7 +281,16 @@ void ourui_OurPreference(laUiList *uil, laPropPack *This, laPropPack *DetachedPr
     laShowLabel(uil,c,"Generic:",0,0);
     laShowItem(uil,cl,0,"our.preferences.enable_brush_circle");
     laShowItem(uil,cr,0,"our.preferences.lock_radius");
+    laShowItem(uil,cl,0,"our.preferences.allow_none_pressure");
+    laShowItem(uil,cr,0,"our.preferences.bad_event_tolerance");
+    laShowItem(uil,cl,0,"our.preferences.spectral_mode");
+    laShowItem(uil,cr,0,"our.preferences.canvas_default_scale");
+
+    laShowSeparator(uil,c);
+
+    laShowLabel(uil,c,"Undo:",0,0);
     laShowItem(uil,c,0,"our.preferences.paint_undo_limit");
+    
     laShowSeparator(uil,c);
 
     laShowLabel(uil,c,"Exporting Defaults:",0,0);
@@ -422,6 +321,7 @@ void ourui_SplashPanel(laUiList *uil, laPropPack *This, laPropPack *DetachedProp
     laShowItemFull(uil, cr, 0, "LA_open_internet_link", 0, "icon=🗩;link=http://www.ChengduLittleA.com/ourpaint;text=Our Paint blog", 0, 0);
     laShowItemFull(uil, cr, 0, "LA_open_internet_link", 0, "link=http://www.ChengduLittleA.com/ourpaintlog;text=Development logs", 0, 0);
     laShowItemFull(uil, cr, 0, "LA_open_internet_link", 0, "icon=📖;link=http://www.ChengduLittleA.com/ourpaintmanual;text=User Manual", 0, 0);
+    laShowItemFull(uil, cr, 0, "LA_open_internet_link", 0, "icon=🐞;link=https://www.wellobserve.com/repositories/chengdulittlea/OurPaint/issues;text=Report a Bug", 0, 0);
     laShowLabel(uil,cr," ",0,0);
     laShowLabel(uil,cr,"Support the development:",0,0)->Flags|=LA_UI_FLAGS_DISABLED|LA_TEXT_LINE_WRAP|LA_UI_MIN_WIDTH;
     b=laBeginRow(uil,cr,1,0);
@@ -572,6 +472,11 @@ void our_CanvasDrawOverlay(laUiItem* ui,int h){
 
     tnsUniformColorMode(T->immShader, 0); tnsUniformInputColorSpace(T->immShader, 0); tnsUniformOutputColorSpace(T->immShader, MAIN.CurrentWindow->OutputColorSpace);
     if(Our->EnableBrushCircle && (!ocd->HideBrushCircle)){ our_CanvasDrawBrushCircle(ocd); }
+
+    if(!(ui->Flags&LA_UI_FLAGS_NO_OVERLAY)){
+        char buf[128]; sprintf(buf,"%.1lf%%",100.0f/e->ZoomX);
+        tnsDrawStringAuto(buf,laThemeColor(bt,LA_BT_TEXT),ui->L+bt->LM,ui->R-bt->RM,ui->B-LA_RH-bt->BM,0);
+    }
     
     la_CanvasDefaultOverlay(ui, h);
 }
@@ -614,8 +519,10 @@ int our_MergeLayer(OurLayer* l){
     glUniform1f(Our->uAlphaBottom, 1-ol->Transparency);
     for(int row=0;row<OUR_TILES_PER_ROW;row++){ if(!l->TexTiles[row]) continue;// Should not happen.
         for(int col=0;col<OUR_TILES_PER_ROW;col++){ if(!l->TexTiles[row][col]) continue; OurTexTile*t=l->TexTiles[row][col];
+            if(!t->Texture) continue;
             int tl,tr,tu,tb; our_LayerEnsureTileDirect(ol,row,col);
-            OurTexTile*ot=ol->TexTiles[row][col]; if(!ot) continue;
+            OurTexTile*ot=ol->TexTiles[row][col];
+            if((!ot) || (!ot->Texture)) our_LayerEnsureTileDirect(ol,row,col);
             glBindImageTexture(0, t->Texture->GLTexHandle, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA16);
             glBindImageTexture(1, ot->Texture->GLTexHandle, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA16);
             glDispatchCompute(32,32,1);
@@ -718,13 +625,13 @@ void ourundo_Free(OurUndo* undo,int FromLeft){
     while(ut=lstPopItem(&undo->Tiles)){ free(ut->CopyData); memFree(ut); }
     memFree(undo);
 }
-#define OUR_XXYY_TO_COL_ROW_RANGE\
+#define OUR_XXYY_TO_COL_ROW_RANGE \
     l=(int)(floor(OUR_TILE_CTR+(xmin-OUR_TILE_SEAM)/OUR_TILE_W_USE+0.5));\
     r=(int)(floor(OUR_TILE_CTR+(xmax+OUR_TILE_SEAM)/OUR_TILE_W_USE+0.5));\
     u=(int)(floor(OUR_TILE_CTR+(ymax+OUR_TILE_SEAM)/OUR_TILE_W_USE+0.5));\
     b=(int)(floor(OUR_TILE_CTR+(ymin-OUR_TILE_SEAM)/OUR_TILE_W_USE+0.5));\
     TNS_CLAMP(l,0,OUR_TILES_PER_ROW-1); TNS_CLAMP(r,0,OUR_TILES_PER_ROW-1); TNS_CLAMP(u,0,OUR_TILES_PER_ROW-1); TNS_CLAMP(b,0,OUR_TILES_PER_ROW-1);
-#define OUR_XXYY_TO_COL_ROW_ALIGNED\
+#define OUR_XXYY_TO_COL_ROW_ALIGNED \
     l=(int)(floor(OUR_TILE_CTR+(xmin)/OUR_TILE_W_USE+0.5));\
     r=(int)(floor(OUR_TILE_CTR+(xmax-1)/OUR_TILE_W_USE+0.5));\
     u=(int)(floor(OUR_TILE_CTR+(ymax-1)/OUR_TILE_W_USE+0.5));\
@@ -863,7 +770,7 @@ void our_CanvasFillImageBufferBackground(){
     }
 }
 void our_LayerToImageBuffer(OurLayer* ol, int composite){
-    if(ol->Hide || ol->Transparency==1) return;
+    if(composite && (ol->Hide || ol->Transparency==1)) return;
     for(int row=0;row<OUR_TILES_PER_ROW;row++){ if(!ol->TexTiles[row]) continue;
         for(int col=0;col<OUR_TILES_PER_ROW;col++){ if(!ol->TexTiles[row][col]) continue;
             int sx=ol->TexTiles[row][col]->l+OUR_TILE_SEAM,sy=ol->TexTiles[row][col]->b+OUR_TILE_SEAM;
@@ -884,7 +791,7 @@ void our_GetFinalDimension(int UseFrame, int* x, int* y, int* w, int* h){
     else{ *x=Our->ImageX; *y=Our->ImageY; *w=Our->ImageW; *h=Our->ImageH; }
     printf("%d %d %d %d, %d %d %d %d\n",Our->X, Our->Y, Our->W, Our->H,Our->ImageX, Our->ImageY, Our->ImageW, Our->ImageH);
 }
-#define GET_FINAL_ROW_TYPE(TYPE)\
+#define GET_FINAL_ROW_TYPE(TYPE) \
 TYPE* our_GetFinalRow_##TYPE(int UseFrame, int row, int x, int y, int w, int h, TYPE* temp){\
     if(!UseFrame) return &((TYPE*)Our->ImageBuffer)[Our->ImageW*(Our->ImageH-row-1)*4];\
     int userow=(h-row-1)-(Our->ImageY-(y-h));\
@@ -1225,7 +1132,7 @@ int our_PaintGetDabs(OurBrush* b, OurLayer* l, real x, real y, real xto, real yt
         od->ResampleSmudge=0;
         if(b->Smudge>1e-3){ b->SmudgeAccum+=step;
             if(b->SmudgeAccum>(b->EvalSmudgeLength*od->Size)){ b->SmudgeAccum-=(b->EvalSmudgeLength*od->Size); od->ResampleSmudge=1; }
-            od->Recentness=b->SmudgeAccum/b->EvalSmudgeLength/od->Size;
+            od->Recentness=b->SmudgeAccum/b->EvalSmudgeLength/od->Size; TNS_CLAMP(od->Recentness,0,1);
         }else{od->Recentness=0;}
         if(step+uselen<alllen)uselen+=step; else break;
     }
@@ -1283,7 +1190,7 @@ STRUCTURE(OurSmudgeSegement){
 };
 void our_PaintDoDabsWithSmudgeSegments(OurLayer* l,int tl, int tr, int tu, int tb){
     laListHandle Segments={0}; int from=0,to=Our->NextDab; if(!Our->NextDab) return;
-    OurSmudgeSegement* oss;
+    OurSmudgeSegement* oss; unsigned int uniforms[2];
     oss=lstAppendPointerSized(&Segments, 0,sizeof(OurSmudgeSegement));
     for(int i=1;i<to;i++){
         if(Our->Dabs[i].ResampleSmudge){ oss->Start=from; oss->End=i; from=i;
@@ -1295,12 +1202,17 @@ void our_PaintDoDabsWithSmudgeSegments(OurLayer* l,int tl, int tr, int tu, int t
 
     glUseProgram(Our->CanvasProgram);
     glUniform1i(Our->uBrushErasing,Our->Erasing);
+    uniforms[Our->uBrushRoutineSelection]=Our->RoutineDoDabs;
+    uniforms[Our->uMixRoutineSelection]=Our->SpectralMode?Our->RoutineDoMixSpectral:Our->RoutineDoMixNormal;
+    glUniformSubroutinesuiv(GL_COMPUTE_SHADER,2,uniforms);
+    
 
     while(oss=lstPopItem(&Segments)){
         if(oss->Resample || Our->CurrentBrush->SmudgeRestart){
-            glUniformSubroutinesuiv(GL_COMPUTE_SHADER,1,&Our->RoutineDoSample);
+            uniforms[Our->uBrushRoutineSelection]=Our->RoutineDoSample;
+            glUniformSubroutinesuiv(GL_COMPUTE_SHADER,2,uniforms);
             int x=Our->Dabs[oss->Start].X, y=Our->Dabs[oss->Start].Y; float usize=Our->Dabs[oss->Start].Size;
-            float ssize=(usize>15)?(usize+1.5):(usize*1.1);
+            float ssize=(usize>15)?(usize+1.5):(usize*1.1); if(ssize<3) ssize=3;
             int colmax=(int)(floor(OUR_TILE_CTR+(float)(x+ssize)/OUR_TILE_W_USE+0.5)); TNS_CLAMP(colmax,0,OUR_TILES_PER_ROW-1);
             int rowmax=(int)(floor(OUR_TILE_CTR+(float)(y+ssize)/OUR_TILE_W_USE+0.5)); TNS_CLAMP(rowmax,0,OUR_TILES_PER_ROW-1);
             int colmin=(int)(floor(OUR_TILE_CTR+(float)(x-ssize)/OUR_TILE_W_USE+0.5)); TNS_CLAMP(colmin,0,OUR_TILES_PER_ROW-1);
@@ -1314,7 +1226,8 @@ void our_PaintDoDabsWithSmudgeSegments(OurLayer* l,int tl, int tr, int tu, int t
                 }
             }
             Our->CurrentBrush->SmudgeRestart=0;
-            glUniformSubroutinesuiv(GL_COMPUTE_SHADER,1,&Our->RoutineDoDabs);
+            uniforms[Our->uBrushRoutineSelection]=Our->RoutineDoDabs;
+            glUniformSubroutinesuiv(GL_COMPUTE_SHADER,2,uniforms);
             glUniform1i(Our->uBrushErasing,Our->Erasing);
         }
 
@@ -1623,6 +1536,7 @@ int ourinv_Action(laOperator* a, laEvent* e){
     ex->CanvasDownX=x; ex->CanvasDownY=y;
     Our->ActiveTool=Our->Tool; Our->CurrentScale = 1.0f/ex->Base.ZoomX;
     Our->xmin=FLT_MAX;Our->xmax=-FLT_MAX;Our->ymin=FLT_MAX;Our->ymax=-FLT_MAX; Our->ResetBrush=1; ex->HideBrushCircle=1;
+    Our->PaintProcessedEvents=0; Our->BadEventsGiveUp=0; Our->BadEventCount=0;
     if(Our->ActiveTool==OUR_TOOL_CROP){ if(!Our->ShowBorder) return LA_FINISHED; our_StartCropping(ex); }
     if(l->Hide || l->Transparency==1 || l->Lock){ return LA_FINISHED; }
     Our->LockBackground=1; laNotifyUsers("our.lock_background");
@@ -1633,19 +1547,33 @@ int ourinv_Action(laOperator* a, laEvent* e){
 int ourmod_Paint(laOperator* a, laEvent* e){
     OurLayer* l=Our->CurrentLayer; OurCanvasDraw *ex = a->This?a->This->EndInstance:0; OurBrush* ob=Our->CurrentBrush; if(!l||!ex||!ob) return LA_CANCELED;
     if(e->Type==LA_L_MOUSE_UP || e->Type==LA_R_MOUSE_DOWN || e->Type==LA_ESCAPE_DOWN){
-        our_RecordUndo(l,Our->xmin,Our->xmax,Our->ymin,Our->ymax,0,1); ex->HideBrushCircle=0; laShowCursor();
+        if(Our->PaintProcessedEvents) our_RecordUndo(l,Our->xmin,Our->xmax,Our->ymin,Our->ymax,0,1);
+        ex->HideBrushCircle=0; laShowCursor();
+        laEvent* ue; while(ue=lstPopItem(&Our->BadEvents)){ memFree(ue); }
         return LA_FINISHED;
     }
 
     if(e->Type==LA_MOUSEMOVE||e->Type==LA_L_MOUSE_DOWN){
-        real x,y; our_UiToCanvas(&ex->Base,e,&x,&y);
-        int tl,tr,tu,tb; if(ex->LastPressure<0){ ex->LastPressure=e->Pressure; }
-        if(our_PaintGetDabs(ob,l,ex->CanvasLastX,ex->CanvasLastY,x,y,
-            ex->LastPressure,ex->LastTilt[0],ex->LastTilt[1],e->Pressure,e->AngleX,e->AngleY,&tl,&tr,&tu,&tb,&ex->CanvasLastX,&ex->CanvasLastY)){
-            our_PaintDoDabsWithSmudgeSegments(l,tl,tr,tu,tb);
-            laNotifyUsers("our.canvas"); laMarkMemChanged(Our->CanvasSaverDummyList.pFirst);
+        if((!e->GoodPressure) && ((!Our->BadEventsGiveUp)||(!Our->AllowNonPressure))){
+            laEvent* be=memAcquire(sizeof(laEvent)); memcpy(be,e,sizeof(laEvent)); be->Item.pNext=be->Item.pPrev=0;
+            lstAppendItem(&Our->BadEvents,be); Our->BadEventCount++;
+            if(Our->BadEventCount>=Our->BadEventsLimit){ Our->BadEventsGiveUp=1; }
+        }else{
+            Our->PaintProcessedEvents=1; laEvent* UseEvent;real Pressure=e->Pressure,AngleX=e->AngleX,AngleY=e->AngleY;
+            while(1){
+                UseEvent=lstPopItem(&Our->BadEvents); if(!UseEvent){ UseEvent=e; }
+                real x,y; our_UiToCanvas(&ex->Base,UseEvent,&x,&y);
+                int tl,tr,tu,tb; if(ex->LastPressure<0){ ex->LastPressure=Pressure; }
+                if(our_PaintGetDabs(ob,l,ex->CanvasLastX,ex->CanvasLastY,x,y,ex->LastPressure,ex->LastTilt[0],ex->LastTilt[1],Pressure,AngleX,AngleY,
+                    &tl,&tr,&tu,&tb,&ex->CanvasLastX,&ex->CanvasLastY)){
+                    our_PaintDoDabsWithSmudgeSegments(l,tl,tr,tu,tb);
+                    laNotifyUsers("our.canvas"); laMarkMemChanged(Our->CanvasSaverDummyList.pFirst);
+                }
+                ex->LastPressure=Pressure;ex->LastTilt[0]=AngleX;ex->LastTilt[1]=AngleY;
+                if(UseEvent==e){ break; }
+                else{ memFree(UseEvent); }
+            }
         }
-        ex->LastPressure=e->Pressure;ex->LastTilt[0]=e->AngleX;ex->LastTilt[1]=e->AngleY;
     }
 
     return LA_RUNNING;
@@ -1782,14 +1710,19 @@ void ourpropagate_Tools(OurPaint* p, laUDF* udf, int force){
 }
 void ourset_CurrentBrush(void* unused, OurBrush* b){
     real r; if(Our->LockRadius) r=Our->CurrentBrush?Our->CurrentBrush->Size:15;
-    Our->CurrentBrush=b; if(b && Our->LockRadius) b->Size=r;
-    if(b->DefaultAsEraser){ Our->Erasing=1; Our->EraserID=b->Binding; }else{ Our->Erasing=0; Our->PenID=b->Binding; }
+    OurBrush* ob=Our->CurrentBrush;
+    if(ob){
+        if(ob->DefaultAsEraser){ Our->SaveEraserSize=ob->Size; }else{ Our->SaveBrushSize=ob->Size; }
+    }
+    Our->CurrentBrush=b;
+    if(b->DefaultAsEraser){ Our->Erasing=1; Our->EraserID=b->Binding; if(Our->LockRadius) b->Size=Our->SaveEraserSize?Our->SaveEraserSize:r; }
+    else{ Our->Erasing=0; Our->PenID=b->Binding; if(Our->LockRadius) b->Size=Our->SaveBrushSize?Our->SaveBrushSize:r; }
     laNotifyUsers("our.tools.current_brush"); laDriverRequestRebuild();
 }
 void ourset_CurrentLayer(void* unused, OurLayer*l){
     memAssignRef(Our, &Our->CurrentLayer, l); laNotifyUsers("our.canvas");
 }
-#define OUR_ADD_PRESSURE_SWITCH(p)\
+#define OUR_ADD_PRESSURE_SWITCH(p) \
     laAddEnumItemAs(p,"NONE","None","Not using pressure",0,0);\
     laAddEnumItemAs(p,"ENABLED","Enabled","Using pressure",1,0);
 
@@ -1819,8 +1752,12 @@ void ourui_MenuButtons(laUiList *uil, laPropPack *pp, laPropPack *actinst, laCol
         mc = laFirstColumn(muil);
         laShowLabel(muil, mc, "Settings", 0, 0)->Flags|=LA_TEXT_MONO|LA_UI_FLAGS_DISABLED;
         laShowItemFull(muil, mc, 0, "LA_panel_activator", 0, "panel_id=LAUI_user_preferences;", 0, 0);
-        laShowLabel(muil, mc, "Information", 0, 0)->Flags|=LA_TEXT_MONO|LA_UI_FLAGS_DISABLED;
+        
+        laShowLabel(muil, mc, "Help", 0, 0)->Flags|=LA_TEXT_MONO|LA_UI_FLAGS_DISABLED;
         laShowItemFull(muil, mc, 0, "LA_open_internet_link", 0, "icon=📖;link=http://www.ChengduLittleA.com/ourpaintmanual;text=User Manual", 0, 0);
+        laShowItemFull(muil, mc, 0, "LA_open_internet_link", 0, "icon=🐞;link=https://www.wellobserve.com/repositories/chengdulittlea/OurPaint/issues;text=Report a Bug", 0, 0);
+        
+        laShowLabel(muil, mc, "Information", 0, 0)->Flags|=LA_TEXT_MONO|LA_UI_FLAGS_DISABLED;
         laShowItemFull(muil, mc, 0, "LA_panel_activator", 0, "panel_id=LAUI_about;text=About;", 0, 0);
     }
 }
@@ -1925,13 +1862,17 @@ void ourRegisterEverything(){
     pc=laAddPropertyContainer("our_preferences","Our Preferences","OurPaint preferences",0,0,sizeof(OurPaint),0,0,1);
     laPropContainerExtraFunctions(pc,0,ourreset_Preferences,0,0,0);
     p=laAddEnumProperty(pc,"lock_radius","Lock Radius","Lock radius when changing brushes",LA_WIDGET_ENUM_HIGHLIGHT,0,0,0,0,offsetof(OurPaint,LockRadius),0,0,0,0,0,0,0,0,0,0);
-    laAddEnumItemAs(p,"FALSE","No","Dont' lock radius",0,0);
+    laAddEnumItemAs(p,"FALSE","No","Dontt lock radius",0,0);
     laAddEnumItemAs(p,"TRUE","Yes","Lock radius when changing brushes",1,0);
     p=laAddEnumProperty(pc,"enable_brush_circle","Brush Circle","Enable brush circle when hovering",LA_WIDGET_ENUM_HIGHLIGHT,0,0,0,0,offsetof(OurPaint,EnableBrushCircle),0,0,0,0,0,0,0,0,0,0);
-    laAddEnumItemAs(p,"FALSE","No","Dont' show brush circle",0,0);
+    laAddEnumItemAs(p,"FALSE","No","Dontt show brush circle",0,0);
     laAddEnumItemAs(p,"TRUE","Yes","Show brush circle on hover",1,0);
+    p=laAddEnumProperty(pc,"allow_none_pressure","Allow Non-pressure","Allow non-pressure events, this enables mouse painting.",LA_WIDGET_ENUM_HIGHLIGHT,0,0,0,0,offsetof(OurPaint,AllowNonPressure),0,0,0,0,0,0,0,0,0,0);
+    laAddEnumItemAs(p,"FALSE","No","Don't allow non-pressure device inputs",0,0);
+    laAddEnumItemAs(p,"TRUE","Yes","Allow non-pressure device inputs such as a mouse",1,0);
+    laAddIntProperty(pc,"bad_event_tolerance","Bad Event Tolerance","Try to recieve more events before painting starts to get around some stylus hardware issue",0,0,0,16,0,1,0,0,offsetof(OurPaint,BadEventsLimit),0,0,0,0,0,0,0,0,0,0,0);
     p=laAddEnumProperty(pc,"show_debug_tiles","Show debug tiles","Whether to show debug tiles",LA_WIDGET_ENUM_HIGHLIGHT,0,0,0,0,offsetof(OurPaint,ShowTiles),0,ourset_ShowTiles,0,0,0,0,0,0,0,0);
-    laAddEnumItemAs(p,"FALSE","No","Dont' show debug tiles on the canvas",0,0);
+    laAddEnumItemAs(p,"FALSE","No","Dontt show debug tiles on the canvas",0,0);
     laAddEnumItemAs(p,"TRUE","Yes","Show debug tiles on the canvas",1,0);
     p=laAddEnumProperty(pc,"export_default_bit_depth","Export Default Bit Depth","Default bit depth when exporting images",0,0,0,0,0,offsetof(OurPaint,DefaultBitDepth),0,0,0,0,0,0,0,0,0,0);
     laAddEnumItemAs(p,"D8","8 Bits","Use 8 bits per channel",OUR_EXPORT_BIT_DEPTH_8,0);
@@ -1941,6 +1882,10 @@ void ourRegisterEverything(){
     laAddEnumItemAs(p,"SRGB","sRGB","Convert pixels into non-linear sRGB (Most used)",OUR_EXPORT_COLOR_MODE_SRGB,0);
     laAddEnumItemAs(p,"CLAY","Clay","Convert pixels into non-linear Clay (AdobeRGB 1998 compatible)",OUR_EXPORT_COLOR_MODE_CLAY,0);
     laAddIntProperty(pc,"paint_undo_limit","Paint Undo Limit","Undo step limit for painting actions.",0,0," Steps",256,5,1,100,0,offsetof(OurPaint,PaintUndoLimit),0,0,0,0,0,0,0,0,0,0,0);
+    laAddFloatProperty(pc,"canvas_default_scale","Canvas Default Scale","Default scale of the canvas",0,0,0,4,0.25,0.1,0.5,0,offsetof(OurPaint,DefaultScale),0,0,0,0,0,0,0,0,0,0,0);
+    p=laAddEnumProperty(pc,"spectral_mode","Spectral Brush","Use spectral mixing in brush strokes",LA_WIDGET_ENUM_HIGHLIGHT,0,0,0,0,offsetof(OurPaint,SpectralMode),0,0,0,0,0,0,0,0,0,0);
+    laAddEnumItemAs(p,"NONE","None","Use regular RGB mixing for brushes",0,0);
+    laAddEnumItemAs(p,"SPECTRAL","Spectral","Use spectral mixing for brushes",1,0);
     
     pc=laAddPropertyContainer("our_tools","Our Tools","OurPaint tools",0,0,sizeof(OurPaint),0,0,1);
     laPropContainerExtraFunctions(pc,0,0,0,ourpropagate_Tools,0);
@@ -2121,19 +2066,30 @@ int ourInit(){
     Our->uBrushRoutineSelection=glGetSubroutineUniformLocation(Our->CanvasProgram, GL_COMPUTE_SHADER, "uBrushRoutineSelection");
     Our->RoutineDoDabs=glGetSubroutineIndex(Our->CanvasProgram, GL_COMPUTE_SHADER, "DoDabs");
     Our->RoutineDoSample=glGetSubroutineIndex(Our->CanvasProgram, GL_COMPUTE_SHADER, "DoSample");
+    
+    Our->uMixRoutineSelection=glGetSubroutineUniformLocation(Our->CanvasProgram, GL_COMPUTE_SHADER, "uMixRoutineSelection");
+    Our->RoutineDoMixNormal=glGetSubroutineIndex(Our->CanvasProgram, GL_COMPUTE_SHADER, "DoMixNormal");
+    Our->RoutineDoMixSpectral=glGetSubroutineIndex(Our->CanvasProgram, GL_COMPUTE_SHADER, "DoMixSpectral");
 
     Our->uBlendMode=glGetUniformLocation(Our->CompositionProgram,"uBlendMode");
     Our->uAlphaTop=glGetUniformLocation(Our->CompositionProgram,"uAlphaTop");
     Our->uAlphaBottom=glGetUniformLocation(Our->CompositionProgram,"uAlphaBottom");
 
+    Our->DefaultScale=0.5;
+
     Our->X=-2800/2; Our->W=2800;
     Our->Y=2400/2;  Our->H=2400;
     Our->BorderAlpha=0.6;
 
+    Our->SpectralMode=1;
+
     Our->LockRadius=1;
     Our->EnableBrushCircle=1;
     Our->PaintUndoLimit=100;
 
+    Our->AllowNonPressure=1;
+    Our->BadEventsLimit=7;
+
     Our->PenID=-1;
     Our->EraserID=-1;
 

+ 1 - 1
ourpaint.c

@@ -23,7 +23,7 @@ extern tnsMain* T;
 extern OurPaint *Our;
 
 int main(int argc, char *argv[]){
-    laGetReady();
+    laGetReadyWith(4,5,0);
 
     if(!ourInit()){ laShutoff(0); return -1; }
 

+ 19 - 0
ourpaint.h

@@ -30,6 +30,15 @@
 extern unsigned char DATA_SPLASH[];
 extern unsigned char DATA_SPLASH_HIGHDPI[];
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+extern const char OUR_CANVAS_SHADER[];
+extern const char OUR_COMPOSITION_SHADER[];
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #define OUR_AT_CROP_CENTER 0
 #define OUR_AT_CROP_L 1
 #define OUR_AT_CROP_R 2
@@ -241,6 +250,8 @@ STRUCTURE(OurPaint){
     laListHandle CanvasSaverDummyList;
     laProp*      CanvasSaverDummyProp;
 
+    laListHandle BadEvents;
+
     tnsImage* SplashImage;
     tnsImage* SplashImageHigh;
 
@@ -248,10 +259,12 @@ STRUCTURE(OurPaint){
     OurLayer*    CurrentLayer;
     laListHandle Brushes;
     OurBrush*    CurrentBrush;
+    real SaveBrushSize,SaveEraserSize;
     OurDab* Dabs; int NextDab,MaxDab;
     laListHandle BrushEval;
 
     real CurrentScale;
+    real DefaultScale;
 
     int Tool,ActiveTool,Erasing,EventErasing;
     int LockBackground;
@@ -260,12 +273,15 @@ STRUCTURE(OurPaint){
     int ColorInterpretation;
     int ShowBorder,UseBorder;
     int ShowTiles;
+    int AllowNonPressure,PaintProcessedEvents;
+    int BadEventsLimit,BadEventCount,BadEventsGiveUp;
 
     int LockRadius;
     int EnableBrushCircle;
     int DefaultBitDepth;
     int DefaultColorProfile;
     int PaintUndoLimit;
+    int SpectralMode;
 
     tnsTexture* SmudgeTexture;
     GLuint CanvasShader;      GLuint CanvasProgram;
@@ -280,9 +296,12 @@ STRUCTURE(OurPaint){
     GLint uBrushSlender;
     GLint uBrushAngle;
     GLint uBrushRoutineSelection;
+    GLint uMixRoutineSelection;
     GLint uBrushErasing;
     GLint RoutineDoDabs;
     GLint RoutineDoSample;
+    GLint RoutineDoMixNormal;
+    GLint RoutineDoMixSpectral;
     GLint uBlendMode;
     GLint uAlphaTop;
     GLint uAlphaBottom;

+ 205 - 0
ourshader.cpp

@@ -0,0 +1,205 @@
+/*
+* Our Paint: A light weight GPU powered painting program.
+* Copyright (C) 2022-2023 Wu Yiming
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "ourpaint.h"
+
+const char OUR_CANVAS_SHADER[]=R"(#version 430
+layout(local_size_x = 32, local_size_y = 32, local_size_z = 1) in;
+layout(rgba16, binding = 0) uniform image2D img;
+layout(rgba16, binding = 1) coherent uniform image2D smudge_buckets;
+uniform ivec2 uBrushCorner;
+uniform vec2 uBrushCenter;
+uniform float uBrushSize;
+uniform float uBrushHardness;
+uniform float uBrushSmudge;
+uniform float uBrushSlender;
+uniform float uBrushAngle;
+uniform float uBrushRecentness;
+uniform vec4 uBrushColor;
+uniform vec4 uBackgroundColor;
+uniform int uBrushErasing;
+const vec4 p1_22=vec4(1.0/2.2,1.0/2.2,1.0/2.2,1.0/2.2);
+const vec4 p22=vec4(2.2,2.2,2.2,2.2);
+const float WGM_EPSILON=0.001f;
+const float T_MATRIX_SMALL[3][10] = {{0.026595621243689,0.049779426257903,0.022449850859496,-0.218453689278271
+,-0.256894883201278,0.445881722194840,0.772365886289756,0.194498761382537
+,0.014038157587820,0.007687264480513}
+,{-0.032601672674412,-0.061021043498478,-0.052490001018404
+,0.206659098273522,0.572496335158169,0.317837248815438,-0.021216624031211
+,-0.019387668756117,-0.001521339050858,-0.000835181622534}
+,{0.339475473216284,0.635401374177222,0.771520797089589,0.113222640692379
+,-0.055251113343776,-0.048222578468680,-0.012966666339586
+,-0.001523814504223,-0.000094718948810,-0.000051604594741}};
+const float spectral_r_small[10] = {0.009281362787953,0.009732627042016,0.011254252737167,0.015105578649573
+,0.024797924177217,0.083622585502406,0.977865045723212,1.000000000000000
+,0.999961046144372,0.999999992756822};
+const float spectral_g_small[10] = {0.002854127435775,0.003917589679914,0.012132151699187,0.748259205918013
+,1.000000000000000,0.865695937531795,0.037477469241101,0.022816789725717
+,0.021747419446456,0.021384940572308};
+const float spectral_b_small[10] = {0.537052150373386,0.546646402401469,0.575501819073983,0.258778829633924
+,0.041709923751716,0.012662638828324,0.007485593127390,0.006766900622462
+,0.006699764779016,0.006676219883241};
+void rgb_to_spectral (vec3 rgb, out float spectral_[10]) {
+    float offset = 1.0 - WGM_EPSILON;
+    float r = rgb.r * offset + WGM_EPSILON;
+    float g = rgb.g * offset + WGM_EPSILON;
+    float b = rgb.b * offset + WGM_EPSILON;
+    float spec_r[10] = {0,0,0,0,0,0,0,0,0,0}; for (int i=0; i < 10; i++) {spec_r[i] = spectral_r_small[i] * r;}
+    float spec_g[10] = {0,0,0,0,0,0,0,0,0,0}; for (int i=0; i < 10; i++) {spec_g[i] = spectral_g_small[i] * g;}
+    float spec_b[10] = {0,0,0,0,0,0,0,0,0,0}; for (int i=0; i < 10; i++) {spec_b[i] = spectral_b_small[i] * b;}
+    for (int i=0; i<10; i++) {spectral_[i] = spec_r[i] + spec_g[i] + spec_b[i];}
+}
+vec3 spectral_to_rgb (float spectral[10]) {
+    float offset = 1.0 - WGM_EPSILON;
+    // We need this tmp. array to allow auto vectorization. <-- How about on GPU?
+    float tmp[3] = {0,0,0};
+    for (int i=0; i<10; i++) {
+        tmp[0] += T_MATRIX_SMALL[0][i] * spectral[i];
+        tmp[1] += T_MATRIX_SMALL[1][i] * spectral[i];
+        tmp[2] += T_MATRIX_SMALL[2][i] * spectral[i];
+    }
+    vec3 rgb_;
+    for (int i=0; i<3; i++) {rgb_[i] = clamp((tmp[i] - WGM_EPSILON) / offset, 0.0f, 1.0f);}
+    return rgb_;
+}
+subroutine vec4 MixRoutines(vec4 a, vec4 b, float fac_a);
+subroutine(MixRoutines) vec4 DoMixNormal(vec4 a, vec4 b, float fac_a){
+    return mix(a,b,1-fac_a);
+}
+subroutine(MixRoutines) vec4 DoMixSpectral(vec4 a, vec4 b, float fac_a){
+    vec4 result = vec4(0,0,0,0);
+    result.a=mix(a.a,b.a,1-fac_a);
+    float spec_a[10] = {0,0,0,0,0,0,0,0,0,0}; rgb_to_spectral(a.rgb, spec_a);
+    float spec_b[10] = {0,0,0,0,0,0,0,0,0,0}; rgb_to_spectral(b.rgb, spec_b);
+    float spectralmix[10] = {0,0,0,0,0,0,0,0,0,0};
+    for (int i=0; i < 10; i++) { spectralmix[i] = pow(spec_a[i], fac_a) * pow(spec_b[i], 1-fac_a); }
+    result.rgb=spectral_to_rgb(spectralmix);
+    return result;
+}
+subroutine uniform MixRoutines uMixRoutineSelection;
+vec4 spectral_mix(vec4 a, vec4 b, float fac_a){
+    return uMixRoutineSelection(a,b,fac_a);
+}
+vec4 spectral_mix_unpre(vec4 colora, vec4 colorb, float fac){
+    vec4 ca=(colora.a==0)?colora:vec4(colora.rgb/colora.a,colora.a);
+    vec4 cb=(colorb.a==0)?colorb:vec4(colorb.rgb/colorb.a,colorb.a);
+    float af=colora.a*(1-fac);
+    float aa=af/(af+(1-af)*colorb.a+0.000001);
+    vec4 result=spectral_mix(ca,cb,aa);
+    result.a=mix(colora.a,colorb.a,fac);
+    return vec4(result.rgb*result.a,result.a);
+}
+float atan2(in float y, in float x){
+    bool s = (abs(x) > abs(y)); return mix(3.1415926535/2.0 - atan(x,y), atan(y,x), s);
+}
+vec2 rotate(vec2 v, float angle) {
+    float s = sin(angle); float c = cos(angle);
+    return mat2(c,-s,s,c) * v;
+}
+float brightness(vec4 color) {
+    return color.r*0.2126+color.b*0.7152+color.g*0.0722;
+}
+vec4 mix_over(vec4 colora, vec4 colorb){
+    vec4 a=(colora.a==0)?colora:vec4(colora.rgb/colora.a,colora.a);
+    vec4 b=(colorb.a==0)?colorb:vec4(colorb.rgb/colorb.a,colorb.a);
+    vec4 m=vec4(0,0,0,0); float aa=colora.a/(colora.a+(1-colora.a)*colorb.a+0.0001);
+    m=spectral_mix(a,b,aa);
+    m.a=colora.a+colorb.a*(1-colora.a);
+    m=vec4(m.rgb*m.a,m.a);
+    return m;
+}
+int dab(float d, vec4 color, float size, float hardness, float smudge, vec4 smudge_color, vec4 last_color, out vec4 final){
+    vec4 cc=color;
+    float fac=1-pow(d/size,1+1/(1-hardness+1e-4));
+    cc.a=color.a*fac*(1-smudge); cc.rgb=cc.rgb*cc.a;
+    float erasing=float(uBrushErasing);
+    cc=cc*(1-erasing);
+    // this looks better than the one commented out below
+    vec4 c2=spectral_mix_unpre(last_color,smudge_color,smudge*fac*color.a);
+    c2=mix_over(cc,c2);
+    //vec4 c2=mix_over(cc,last_color);
+    //c2=spectral_mix_unpre(c2,smudge_color,smudge*fac*color.a);
+    c2=spectral_mix_unpre(c2,c2*(1-fac*color.a),erasing);
+    final=c2;
+    return 1;
+}
+subroutine void BrushRoutines();
+subroutine(BrushRoutines) void DoDabs(){
+    ivec2 px = ivec2(gl_GlobalInvocationID.xy)+uBrushCorner;
+    if(px.x<0||px.y<0||px.x>1024||px.y>1024) return;
+    vec2 fpx=vec2(px);
+    fpx=uBrushCenter+rotate(fpx-uBrushCenter,uBrushAngle);
+    fpx.x=uBrushCenter.x+(fpx.x-uBrushCenter.x)*(1+uBrushSlender);
+    float dd=distance(fpx,uBrushCenter); if(dd>uBrushSize) return;
+    vec4 dabc=imageLoad(img, px);
+    vec4 smudgec=pow(spectral_mix_unpre(pow(imageLoad(smudge_buckets,ivec2(1,0)),p1_22),pow(imageLoad(smudge_buckets,ivec2(0,0)),p1_22),uBrushRecentness),p22);
+    vec4 final_color;
+    dab(dd,uBrushColor,uBrushSize,uBrushHardness,uBrushSmudge,smudgec,dabc,final_color);
+    dabc=final_color;
+    imageStore(img, px, dabc);
+}
+subroutine(BrushRoutines) void DoSample(){
+    ivec2 p=ivec2(gl_GlobalInvocationID.xy);
+    int DoSample=1; vec4 color;
+    if(p.y==0){
+        vec2 sp=round(vec2(sin(float(p.x)),cos(float(p.x)))*uBrushSize);
+        ivec2 px=ivec2(sp)+uBrushCorner; if(px.x<0||px.y<0||px.x>=1024||px.y>=1024){ DoSample=0; }
+        if(DoSample!=0){
+            ivec2 b=uBrushCorner; if(b.x>=0&&b.y>=0&&b.x<1024&&b.y<1024){ imageStore(smudge_buckets,ivec2(128+32,0),imageLoad(img, b)); }
+            color=imageLoad(img, px);
+            imageStore(smudge_buckets,ivec2(p.x+128,0),color);
+        }
+    }else{DoSample=0;}
+    memoryBarrier();barrier(); if(DoSample==0) return;
+    if(uBrushErasing==0 || p.x!=0) return;
+    color=vec4(0,0,0,0); for(int i=0;i<32;i++){ color=color+imageLoad(smudge_buckets, ivec2(i+128,0)); }
+    color=spectral_mix_unpre(color/32,imageLoad(smudge_buckets, ivec2(128+32,0)),0.6*(1-uBrushColor.a)); vec4 oldcolor=imageLoad(smudge_buckets, ivec2(0,0));
+    imageStore(smudge_buckets,ivec2(1,0),uBrushErasing==2?color:oldcolor);
+    imageStore(smudge_buckets,ivec2(0,0),color);
+}
+subroutine uniform BrushRoutines uBrushRoutineSelection;
+void main() {
+    uBrushRoutineSelection();
+}
+)";
+
+const char OUR_COMPOSITION_SHADER[]=R"(#version 430
+layout(local_size_x = 32, local_size_y = 32, local_size_z = 1) in;
+layout(rgba16, binding = 0) uniform image2D top;
+layout(rgba16, binding = 1) uniform image2D bottom;
+uniform int uBlendMode;
+uniform float uAlphaTop;
+uniform float uAlphaBottom;
+vec4 mix_over(vec4 colora, vec4 colorb){
+    colora=colora*uAlphaTop/uAlphaBottom;
+    vec4 c; c.a=colora.a+colorb.a*(1-colora.a);
+    c.rgb=(colora.rgb+colorb.rgb*(1-colora.a));
+    return c;
+}
+vec4 add_over(vec4 colora, vec4 colorb){
+    colora=colora*uAlphaTop/uAlphaBottom;
+    vec4 a=colora+colorb; a.a=clamp(a.a,0,1); return a;
+}
+void main() {
+    ivec2 px=ivec2(gl_GlobalInvocationID.xy);
+    vec4 c1=imageLoad(top,px); vec4 c2=imageLoad(bottom,px);
+    vec4 c=(uBlendMode==0)?mix_over(c1,c2):add_over(c1,c2);
+    imageStore(bottom,px,c);
+    imageStore(top,px,vec4(0,0,0,0));
+}
+)";

+ 8 - 0
ourtranslations.c

@@ -23,6 +23,14 @@ extern tnsMain* T;
 extern OurPaint *Our;
 
 static const char *entries[]={
+"Spectral Brush","光谱笔刷",
+"Spectral","光谱",
+"Canvas Default Scale","画布默认比例",
+"Help","帮助",
+"Report a Bug","报告程序问题",
+"Bad Event Tolerance","容忍错误笔触事件",
+"Allow Non-pressure","允许非压感",
+"Undo:","撤销:",
 "Website","网站",
 "Save on exit:","退出时保存:",
 "Cover artist:","封面作者:",